summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 09:40:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 09:40:42 +0000
commitee664acb356f8123f4f6b00b73c1e1cf0866c7fb (patch)
treef8479f94a28f66654c6a4f6fb99bad6b4e86a40e /app
parent62f7d5c5b69180e82ae8196b7b429eeffc8e7b4f (diff)
downloadgitlab-ce-ee664acb356f8123f4f6b00b73c1e1cf0866c7fb.tar.gz
Add latest changes from gitlab-org/gitlab@15-5-stable-eev15.5.0-rc42
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/access_tokens/components/access_token_table_app.vue26
-rw-r--r--app/assets/javascripts/access_tokens/components/new_access_token_app.vue14
-rw-r--r--app/assets/javascripts/access_tokens/components/tokens_app.vue2
-rw-r--r--app/assets/javascripts/access_tokens/index.js11
-rw-r--r--app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue4
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/actions.js6
-rw-r--r--app/assets/javascripts/admin/background_migrations/components/database_listbox.vue2
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/base.vue112
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue113
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/index.js21
-rw-r--r--app/assets/javascripts/admin/deploy_keys/components/table.vue4
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/actions.js4
-rw-r--r--app/assets/javascripts/admin/users/components/users_table.vue4
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue7
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue42
-rw-r--r--app/assets/javascripts/alerts_settings/utils/cache_updates.js4
-rw-r--r--app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue4
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue4
-rw-r--r--app/assets/javascripts/api/projects_api.js8
-rw-r--r--app/assets/javascripts/api/user_api.js4
-rw-r--r--app/assets/javascripts/awards_handler.js4
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue14
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue8
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js12
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js19
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js4
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js14
-rw-r--r--app/assets/javascripts/blame/blame_redirect.js23
-rw-r--r--app/assets/javascripts/blob/3d_viewer/index.js2
-rw-r--r--app/assets/javascripts/blob/3d_viewer/mesh_object.js2
-rw-r--r--app/assets/javascripts/blob/blob_line_permalink_updater.js1
-rw-r--r--app/assets/javascripts/blob/components/table_contents.vue14
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js4
-rw-r--r--app/assets/javascripts/blob/openapi/index.js4
-rw-r--r--app/assets/javascripts/blob/viewer/index.js6
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js4
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js6
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue10
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue14
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue7
-rw-r--r--app/assets/javascripts/boards/constants.js11
-rw-r--r--app/assets/javascripts/boards/index.js1
-rw-r--r--app/assets/javascripts/branches/divergence_graph.js4
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue29
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue58
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue55
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue62
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue3
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue4
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql8
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql8
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql8
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/settings.js (renamed from app/assets/javascripts/ci_variable_list/graphql/resolvers.js)113
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js8
-rw-r--r--app/assets/javascripts/ci_variable_list/store/actions.js12
-rw-r--r--app/assets/javascripts/ci_variable_list/utils.js10
-rw-r--r--app/assets/javascripts/clusters/agents/components/activity_events_list.vue6
-rw-r--r--app/assets/javascripts/clusters/agents/components/activity_history_item.vue10
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js4
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js4
-rw-r--r--app/assets/javascripts/code_navigation/utils/dom_utils.js14
-rw-r--r--app/assets/javascripts/code_navigation/utils/index.js8
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue11
-rw-r--r--app/assets/javascripts/commit_merge_requests.js4
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue4
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue20
-rw-r--r--app/assets/javascripts/content_editor/components/editor_state_observer.vue4
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue264
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/label.vue34
-rw-r--r--app/assets/javascripts/content_editor/constants/index.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/diagram.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/external_keydown_handler.js38
-rw-r--r--app/assets/javascripts/content_editor/extensions/heading.js16
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference.js14
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference_label.js35
-rw-r--r--app/assets/javascripts/content_editor/extensions/suggestions.js227
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js6
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js7
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js5
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue2
-rw-r--r--app/assets/javascripts/contributors/stores/actions.js4
-rw-r--r--app/assets/javascripts/crm/contacts/components/contacts_root.vue11
-rw-r--r--app/assets/javascripts/crm/organizations/components/organizations_root.vue9
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js6
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue2
-rw-r--r--app/assets/javascripts/deploy_freeze/store/actions.js8
-rw-r--r--app/assets/javascripts/deploy_freeze/store/mutations.js4
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue8
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue332
-rw-r--r--app/assets/javascripts/deploy_tokens/index.js33
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue71
-rw-r--r--app/assets/javascripts/design_management/mixins/all_designs.js8
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue4
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue10
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js6
-rw-r--r--app/assets/javascripts/diff.js4
-rw-r--r--app/assets/javascripts/diffs/components/app.vue10
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue4
-rw-r--r--app/assets/javascripts/diffs/components/commit_widget.vue2
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js6
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue18
-rw-r--r--app/assets/javascripts/diffs/mixins/draft_comments.js2
-rw-r--r--app/assets/javascripts/diffs/store/actions.js11
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js4
-rw-r--r--app/assets/javascripts/editor/schema/ci.json624
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue6
-rw-r--r--app/assets/javascripts/environments/components/deployment.vue4
-rw-r--r--app/assets/javascripts/environments/components/edit_environment.vue4
-rw-r--r--app/assets/javascripts/environments/components/empty_state.vue45
-rw-r--r--app/assets/javascripts/environments/components/enable_review_app_modal.vue160
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue16
-rw-r--r--app/assets/javascripts/environments/components/environment_folder.vue10
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue60
-rw-r--r--app/assets/javascripts/environments/components/environments_detail_header.vue37
-rw-r--r--app/assets/javascripts/environments/components/new_environment.vue4
-rw-r--r--app/assets/javascripts/environments/constants.js31
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql4
-rw-r--r--app/assets/javascripts/environments/graphql/queries/folder.query.graphql4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js8
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js8
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue8
-rw-r--r--app/assets/javascripts/error_tracking/store/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/details/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/app.vue2
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/actions.js4
-rw-r--r--app/assets/javascripts/feature_flags/components/environments_dropdown.vue4
-rw-r--r--app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue4
-rw-r--r--app/assets/javascripts/feature_flags/store/edit/actions.js4
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_ajax_filter.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js4
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js6
-rw-r--r--app/assets/javascripts/flash.js2
-rw-r--r--app/assets/javascripts/gpg_badges.js4
-rw-r--r--app/assets/javascripts/grafana_integration/store/actions.js4
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js52
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json2
-rw-r--r--app/assets/javascripts/graphql_shared/queries/users_search.query.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql6
-rw-r--r--app/assets/javascripts/group.js6
-rw-r--r--app/assets/javascripts/groups/components/app.vue28
-rw-r--r--app/assets/javascripts/groups/components/groups.vue20
-rw-r--r--app/assets/javascripts/groups/components/new_top_level_group_alert.vue40
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue177
-rw-r--r--app/assets/javascripts/groups/components/transfer_group_form.vue2
-rw-r--r--app/assets/javascripts/groups/constants.js25
-rw-r--r--app/assets/javascripts/groups/init_overview_tabs.js2
-rw-r--r--app/assets/javascripts/groups/settings/components/access_dropdown.vue4
-rw-r--r--app/assets/javascripts/groups_select.js12
-rw-r--r--app/assets/javascripts/header_search/components/app.vue19
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue7
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue23
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue6
-rw-r--r--app/assets/javascripts/ide/index.js4
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js4
-rw-r--r--app/assets/javascripts/ide/utils.js22
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue28
-rw-r--r--app/assets/javascripts/import_entities/import_groups/services/status_poller.js4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue51
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue30
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue6
-rw-r--r--app/assets/javascripts/import_entities/import_projects/index.js1
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js29
-rw-r--r--app/assets/javascripts/integrations/constants.js2
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue87
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js56
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue)7
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue51
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/constants.js22
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/index.js25
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js2
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js28
-rw-r--r--app/assets/javascripts/issuable/issuable_context.js19
-rw-r--r--app/assets/javascripts/issuable/issuable_form.js52
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue7
-rw-r--r--app/assets/javascripts/issues/list/constants.js10
-rw-r--r--app/assets/javascripts/issues/show/components/edited.vue33
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue31
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue79
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue7
-rw-r--r--app/assets/javascripts/issues/show/index.js1
-rw-r--r--app/assets/javascripts/jobs/components/table/constants.js19
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue4
-rw-r--r--app/assets/javascripts/jobs/store/actions.js12
-rw-r--r--app/assets/javascripts/labels/components/promote_label_modal.vue4
-rw-r--r--app/assets/javascripts/labels/group_label_subscription.js6
-rw-r--r--app/assets/javascripts/labels/label_manager.js6
-rw-r--r--app/assets/javascripts/labels/labels_select.js6
-rw-r--r--app/assets/javascripts/labels/project_label_subscription.js4
-rw-r--r--app/assets/javascripts/lib/dompurify.js13
-rw-r--r--app/assets/javascripts/lib/utils/autosave.js40
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js51
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js18
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_format_utility.js26
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js61
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js14
-rw-r--r--app/assets/javascripts/listbox/index.js48
-rw-r--r--app/assets/javascripts/logo.js2
-rw-r--r--app/assets/javascripts/main.js7
-rw-r--r--app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue6
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue5
-rw-r--r--app/assets/javascripts/members/utils.js5
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue4
-rw-r--r--app/assets/javascripts/merge_conflicts/store/actions.js4
-rw-r--r--app/assets/javascripts/merge_request.js6
-rw-r--r--app/assets/javascripts/merge_request_tabs.js6
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue1
-rw-r--r--app/assets/javascripts/milestones/components/promote_milestone_modal.vue4
-rw-r--r--app/assets/javascripts/milestones/milestone.js4
-rw-r--r--app/assets/javascripts/milestones/milestone_select.js2
-rw-r--r--app/assets/javascripts/mirrors/mirror_repos.js4
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.js4
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue8
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js30
-rw-r--r--app/assets/javascripts/mr_notes/index.js3
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js13
-rw-r--r--app/assets/javascripts/namespaces/leave_by_url.js4
-rw-r--r--app/assets/javascripts/nav/components/top_nav_app.vue10
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue2
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue9
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue19
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue7
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue67
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue19
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue38
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue24
-rw-r--r--app/assets/javascripts/notes/components/timeline_toggle.vue1
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue4
-rw-r--r--app/assets/javascripts/notes/discussion_filters.js34
-rw-r--r--app/assets/javascripts/notes/i18n.js2
-rw-r--r--app/assets/javascripts/notes/index.js13
-rw-r--r--app/assets/javascripts/notes/mixins/diff_line_note_form.js6
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js156
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js4
-rw-r--r--app/assets/javascripts/notes/timeline.js16
-rw-r--r--app/assets/javascripts/notes/utils/get_notes_filter_data.js21
-rw-r--r--app/assets/javascripts/operation_settings/store/actions.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js10
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue11
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js12
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue18
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue11
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js11
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue60
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_downloader.js4
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_previewer.js4
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js4
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/index.js9
-rw-r--r--app/assets/javascripts/pages/admin/dashboard/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/groups/show/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue4
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js6
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js8
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js4
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js9
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/settings/index.js2
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue4
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue95
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js20
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_history_app.vue4
-rw-r--r--app/assets/javascripts/pages/profiles/index.js2
-rw-r--r--app/assets/javascripts/pages/profiles/init_timezone_dropdown.js34
-rw-r--r--app/assets/javascripts/pages/projects/blame/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue4
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue4
-rw-r--r--app/assets/javascripts/pages/projects/hooks/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js4
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js9
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js15
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js3
-rw-r--r--app/assets/javascripts/pages/projects/project.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue30
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js4
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue4
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue221
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js6
-rw-r--r--app/assets/javascripts/pdf/index.vue17
-rw-r--r--app/assets/javascripts/persistent_user_callout.js6
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue48
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js3
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue4
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue2
-rw-r--r--app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue2
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue254
-rw-r--r--app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql9
-rw-r--r--app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql11
-rw-r--r--app/assets/javascripts/pipeline_new/graphql/resolvers.js29
-rw-r--r--app/assets/javascripts/pipeline_new/index.js14
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue134
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue18
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue66
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue32
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue32
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue29
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue36
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue95
-rw-r--r--app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql6
-rw-r--r--app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql40
-rw-r--r--app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js32
-rw-r--r--app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js32
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/widgets/list.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/jobs_app.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_tabs.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue4
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines_mixin.js4
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js16
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js4
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutations.js4
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue6
-rw-r--r--app/assets/javascripts/profile/gl_crop.js1
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue16
-rw-r--r--app/assets/javascripts/profile/profile.js14
-rw-r--r--app/assets/javascripts/project_select.js4
-rw-r--r--app/assets/javascripts/projects/commit/store/actions.js4
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue8
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue4
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js4
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue46
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown.vue6
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue4
-rw-r--r--app/assets/javascripts/projects/compare/index.js3
-rw-r--r--app/assets/javascripts/projects/project_find_file.js4
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js6
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/branch_dropdown.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue)2
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/index.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/index.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/protections/index.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/merge_protections.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/protections/merge_protections.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/protections/push_protections.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js42
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue207
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue99
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue110
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js11
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql50
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue6
-rw-r--r--app/assets/javascripts/projects/settings/components/default_branch_selector.vue38
-rw-r--r--app/assets/javascripts/projects/settings/components/transfer_project_form.vue170
-rw-r--r--app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql9
-rw-r--r--app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql24
-rw-r--r--app/assets/javascripts/projects/settings/init_transfer_project_form.js2
-rw-r--r--app/assets/javascripts/projects/settings/mount_default_branch_selector.js22
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue9
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue37
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js12
-rw-r--r--app/assets/javascripts/projects/star.js6
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue4
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js6
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js6
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js4
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue174
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue4
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_root.vue12
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue4
-rw-r--r--app/assets/javascripts/releases/components/app_show.vue4
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue92
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js17
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/getters.js1
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js1
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutations.js4
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/state.js2
-rw-r--r--app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue61
-rw-r--r--app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue65
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/actions.js76
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/getters.js45
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/index.js17
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/mutations.js20
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/state.js28
-rw-r--r--app/assets/javascripts/reports/components/issue_body.js3
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue35
-rw-r--r--app/assets/javascripts/repository/commits_service.js4
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue4
-rw-r--r--app/assets/javascripts/repository/components/blob_controls.vue34
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue4
-rw-r--r--app/assets/javascripts/repository/components/new_directory_modal.vue4
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue6
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue4
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue4
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue38
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue7
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_owner_cell.vue63
-rw-r--r--app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue16
-rw-r--r--app/assets/javascripts/runner/components/runner_delete_button.vue32
-rw-r--r--app/assets/javascripts/runner/components/runner_details.vue7
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue1
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue44
-rw-r--r--app/assets/javascripts/runner/components/runner_list_empty_state.vue9
-rw-r--r--app/assets/javascripts/runner/components/runner_membership_toggle.vue42
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token.vue18
-rw-r--r--app/assets/javascripts/runner/constants.js19
-rw-r--r--app/assets/javascripts/runner/graphql/list/group_runners.query.graphql5
-rw-r--r--app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql3
-rw-r--r--app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql14
-rw-r--r--app/assets/javascripts/runner/graphql/list/local_state.js22
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue50
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js6
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js18
-rw-r--r--app/assets/javascripts/search/index.js10
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue25
-rw-r--r--app/assets/javascripts/search/sidebar/index.js9
-rw-r--r--app/assets/javascripts/search/store/actions.js10
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue47
-rw-r--r--app/assets/javascripts/search_settings/components/search_settings.vue41
-rw-r--r--app/assets/javascripts/search_settings/mount.js1
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue11
-rw-r--r--app/assets/javascripts/security_configuration/components/upgrade_banner.vue2
-rw-r--r--app/assets/javascripts/service_ping_consent.js4
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_form.vue10
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue16
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue15
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue8
-rw-r--r--app/assets/javascripts/sidebar/constants.js7
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js6
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js2
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_participants.query.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebar_details.query.graphql9
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebar_details_mr.query.graphql9
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js33
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js8
-rw-r--r--app/assets/javascripts/single_file_diff.js4
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue8
-rw-r--r--app/assets/javascripts/task_list.js4
-rw-r--r--app/assets/javascripts/token_access/components/token_access.vue32
-rw-r--r--app/assets/javascripts/users_select/index.js31
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue314
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue137
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue119
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue98
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue67
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue49
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue68
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue40
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/confidentiality_badge.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/gitlab_version_check.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/utils.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_epics.query.graphql (renamed from app/assets/javascripts/boards/graphql/board_blocking_epics.query.graphql)2
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_issues.query.graphql (renamed from app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql)2
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue (renamed from app/assets/javascripts/boards/components/board_blocked_icon.vue)21
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue47
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue216
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/metric_images/store/actions.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue (renamed from app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue)16
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/history_item.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/registry_search.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql4
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js30
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js45
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js41
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown.vue88
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue119
-rw-r--r--app/assets/javascripts/vue_shared/components/url_sync.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue71
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue117
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue114
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue70
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue117
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue1
-rw-r--r--app/assets/javascripts/vue_shared/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/directives/safe_html.js25
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue11
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue4
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_app.vue134
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_mask_item.vue90
-rw-r--r--app/assets/javascripts/webhooks/index.js25
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue19
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue151
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue118
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue32
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue248
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue2
-rw-r--r--app/assets/javascripts/work_items/constants.js15
-rw-r--r--app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql9
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql16
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql9
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql21
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql19
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql16
-rw-r--r--app/assets/javascripts/work_items/index.js10
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue43
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss7
-rw-r--r--app/assets/stylesheets/bootstrap_migration_reset.scss4
-rw-r--r--app/assets/stylesheets/components/batch_comments/review_bar.scss71
-rw-r--r--app/assets/stylesheets/components/date_time_picker.scss5
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss193
-rw-r--r--app/assets/stylesheets/components/design_management/design_list_item.scss19
-rw-r--r--app/assets/stylesheets/components/feature_highlight.scss5
-rw-r--r--app/assets/stylesheets/components/milestone_combobox.scss5
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss41
-rw-r--r--app/assets/stylesheets/components/release_block.scss3
-rw-r--r--app/assets/stylesheets/components/shortcuts_help.scss (renamed from app/assets/stylesheets/pages/help.scss)11
-rw-r--r--app/assets/stylesheets/framework.scss4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/calendar.scss8
-rw-r--r--app/assets/stylesheets/framework/ci_variable_list.scss77
-rw-r--r--app/assets/stylesheets/framework/diffs.scss247
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss1
-rw-r--r--app/assets/stylesheets/framework/flash.scss27
-rw-r--r--app/assets/stylesheets/framework/flex_grid.scss52
-rw-r--r--app/assets/stylesheets/framework/gfm.scss6
-rw-r--r--app/assets/stylesheets/framework/lists.scss7
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss29
-rw-r--r--app/assets/stylesheets/framework/memory_graph.scss4
-rw-r--r--app/assets/stylesheets/framework/tables.scss16
-rw-r--r--app/assets/stylesheets/framework/timeline.scss26
-rw-r--r--app/assets/stylesheets/framework/toggle.scss131
-rw-r--r--app/assets/stylesheets/framework/typography.scss12
-rw-r--r--app/assets/stylesheets/framework/variables.scss21
-rw-r--r--app/assets/stylesheets/framework/wells.scss9
-rw-r--r--app/assets/stylesheets/highlight/themes/dark.scss2
-rw-r--r--app/assets/stylesheets/highlight/themes/monokai.scss2
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-dark.scss2
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss4
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss2
-rw-r--r--app/assets/stylesheets/lazy_bundles/gridstack.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/admin/geo_nodes.scss45
-rw-r--r--app/assets/stylesheets/page_bundles/admin/geo_replicable.scss18
-rw-r--r--app/assets/stylesheets/page_bundles/cluster_agents.scss13
-rw-r--r--app/assets/stylesheets/page_bundles/clusters.scss (renamed from app/assets/stylesheets/pages/clusters.scss)14
-rw-r--r--app/assets/stylesheets/page_bundles/graph_charts.scss27
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss7
-rw-r--r--app/assets/stylesheets/page_bundles/incidents.scss73
-rw-r--r--app/assets/stylesheets/page_bundles/issues_show.scss214
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss323
-rw-r--r--app/assets/stylesheets/page_bundles/milestone.scss35
-rw-r--r--app/assets/stylesheets/page_bundles/operations.scss (renamed from app/assets/stylesheets/components/dashboard_skeleton.scss)24
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline_schedules.scss96
-rw-r--r--app/assets/stylesheets/page_bundles/profile.scss58
-rw-r--r--app/assets/stylesheets/page_bundles/project.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/prometheus.scss (renamed from app/assets/stylesheets/pages/prometheus.scss)17
-rw-r--r--app/assets/stylesheets/page_bundles/releases.scss (renamed from app/assets/stylesheets/components/release_block_milestone_info.scss)6
-rw-r--r--app/assets/stylesheets/page_bundles/tree.scss (renamed from app/assets/stylesheets/pages/tree.scss)69
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss19
-rw-r--r--app/assets/stylesheets/pages/deploy_keys.scss4
-rw-r--r--app/assets/stylesheets/pages/environment_logs.scss54
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss126
-rw-r--r--app/assets/stylesheets/pages/issues.scss5
-rw-r--r--app/assets/stylesheets/pages/notes.scss336
-rw-r--r--app/assets/stylesheets/pages/profile.scss105
-rw-r--r--app/assets/stylesheets/pages/projects.scss63
-rw-r--r--app/assets/stylesheets/pages/search.scss19
-rw-r--r--app/assets/stylesheets/pages/service_desk.scss7
-rw-r--r--app/assets/stylesheets/pages/settings.scss25
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss27
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss22
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss9
-rw-r--r--app/assets/stylesheets/themes/_dark.scss10
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss5
-rw-r--r--app/assets/stylesheets/utilities.scss131
-rw-r--r--app/components/pajamas/alert_component.rb8
-rw-r--r--app/components/pajamas/progress_component.html.haml2
-rw-r--r--app/components/pajamas/progress_component.rb12
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb3
-rw-r--r--app/controllers/admin/groups_controller.rb11
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb19
-rw-r--r--app/controllers/admin/runners_controller.rb7
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/autocomplete_controller.rb20
-rw-r--r--app/controllers/boards/application_controller.rb23
-rw-r--r--app/controllers/boards/issues_controller.rb162
-rw-r--r--app/controllers/boards/lists_controller.rb103
-rw-r--r--app/controllers/concerns/access_tokens_actions.rb16
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb5
-rw-r--r--app/controllers/concerns/boards_actions.rb59
-rw-r--r--app/controllers/concerns/boards_responses.rb94
-rw-r--r--app/controllers/concerns/import/github_oauth.rb100
-rw-r--r--app/controllers/concerns/issuable_collections_action.rb5
-rw-r--r--app/controllers/concerns/multiple_boards_actions.rb93
-rw-r--r--app/controllers/concerns/preview_markdown.rb16
-rw-r--r--app/controllers/concerns/product_analytics_tracking.rb9
-rw-r--r--app/controllers/concerns/registrations_tracking.rb15
-rw-r--r--app/controllers/concerns/sends_blob.rb22
-rw-r--r--app/controllers/concerns/wiki_actions.rb6
-rw-r--r--app/controllers/groups/application_controller.rb4
-rw-r--r--app/controllers/groups/boards_controller.rb15
-rw-r--r--app/controllers/groups/runners_controller.rb16
-rw-r--r--app/controllers/groups/settings/access_tokens_controller.rb6
-rw-r--r--app/controllers/groups_controller.rb6
-rw-r--r--app/controllers/ide_controller.rb5
-rw-r--r--app/controllers/import/bulk_imports_controller.rb8
-rw-r--r--app/controllers/import/github_controller.rb89
-rw-r--r--app/controllers/import/github_groups_controller.rb57
-rw-r--r--app/controllers/jira_connect/public_keys_controller.rb24
-rw-r--r--app/controllers/oauth/applications_controller.rb1
-rw-r--r--app/controllers/oauth/authorizations_controller.rb1
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb11
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb6
-rw-r--r--app/controllers/profiles/preferences_controller.rb4
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb60
-rw-r--r--app/controllers/projects/application_controller.rb4
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb7
-rw-r--r--app/controllers/projects/blame_controller.rb3
-rw-r--r--app/controllers/projects/boards_controller.rb18
-rw-r--r--app/controllers/projects/compare_controller.rb11
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb6
-rw-r--r--app/controllers/projects/google_cloud/databases_controller.rb25
-rw-r--r--app/controllers/projects/incident_management/timeline_events_controller.rb16
-rw-r--r--app/controllers/projects/incidents_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb47
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests_controller.rb9
-rw-r--r--app/controllers/projects/milestones_controller.rb14
-rw-r--r--app/controllers/projects/pages_domains_controller.rb19
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb5
-rw-r--r--app/controllers/projects/product_analytics_controller.rb4
-rw-r--r--app/controllers/projects/protected_refs_controller.rb1
-rw-r--r--app/controllers/projects/settings/access_tokens_controller.rb6
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/web_ide_terminals_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb10
-rw-r--r--app/controllers/registrations/welcome_controller.rb7
-rw-r--r--app/controllers/registrations_controller.rb12
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/controllers/sessions_controller.rb4
-rw-r--r--app/controllers/users/namespace_callouts_controller.rb17
-rw-r--r--app/controllers/users_controller.rb5
-rw-r--r--app/events/pages_domains/pages_domain_created_event.rb18
-rw-r--r--app/events/pages_domains/pages_domain_deleted_event.rb18
-rw-r--r--app/events/pages_domains/pages_domain_updated_event.rb18
-rw-r--r--app/events/projects/project_attributes_changed_event.rb29
-rw-r--r--app/events/projects/project_features_changed_event.rb22
-rw-r--r--app/finders/ci/runners_finder.rb6
-rw-r--r--app/finders/clusters/agent_authorizations_finder.rb36
-rw-r--r--app/finders/groups/accepting_group_transfers_finder.rb7
-rw-r--r--app/finders/labels_finder.rb2
-rw-r--r--app/finders/packages/group_packages_finder.rb2
-rw-r--r--app/finders/packages/helm/packages_finder.rb2
-rw-r--r--app/finders/packages/nuget/package_finder.rb2
-rw-r--r--app/finders/packages/package_finder.rb2
-rw-r--r--app/finders/packages/packages_finder.rb2
-rw-r--r--app/finders/personal_access_tokens_finder.rb42
-rw-r--r--app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb21
-rw-r--r--app/graphql/gitlab_schema.rb13
-rw-r--r--app/graphql/graphql_triggers.rb20
-rw-r--r--app/graphql/mutations/alert_management/create_alert_issue.rb4
-rw-r--r--app/graphql/mutations/ci/job/artifacts_destroy.rb21
-rw-r--r--app/graphql/mutations/ci/pipeline_schedule/base.rb21
-rw-r--r--app/graphql/mutations/ci/pipeline_schedule/delete.rb27
-rw-r--r--app/graphql/mutations/ci/project_ci_cd_settings_update.rb11
-rw-r--r--app/graphql/mutations/ci/runner/update.rb2
-rw-r--r--app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb3
-rw-r--r--app/graphql/mutations/issues/create.rb8
-rw-r--r--app/graphql/mutations/namespace/package_settings/update.rb30
-rw-r--r--app/graphql/mutations/packages/bulk_destroy.rb43
-rw-r--r--app/graphql/mutations/packages/destroy_files.rb4
-rw-r--r--app/graphql/mutations/work_items/update_widgets.rb60
-rw-r--r--app/graphql/resolvers/base_issues_resolver.rb2
-rw-r--r--app/graphql/resolvers/bulk_labels_resolver.rb27
-rw-r--r--app/graphql/resolvers/ci/all_jobs_resolver.rb17
-rw-r--r--app/graphql/resolvers/ci/runner_projects_resolver.rb4
-rw-r--r--app/graphql/resolvers/concerns/looks_ahead.rb27
-rw-r--r--app/graphql/resolvers/concerns/resolves_merge_requests.rb4
-rw-r--r--app/graphql/resolvers/down_votes_count_resolver.rb15
-rw-r--r--app/graphql/resolvers/project_pipeline_schedules_resolver.rb17
-rw-r--r--app/graphql/resolvers/projects/branch_rules_resolver.rb8
-rw-r--r--app/graphql/resolvers/projects_resolver.rb4
-rw-r--r--app/graphql/resolvers/up_votes_count_resolver.rb15
-rw-r--r--app/graphql/resolvers/work_items_resolver.rb26
-rw-r--r--app/graphql/types/ci/ci_cd_setting_type.rb11
-rw-r--r--app/graphql/types/ci/config_variable_type.rb4
-rw-r--r--app/graphql/types/ci/job_type.rb18
-rw-r--r--app/graphql/types/ci/pipeline_schedule_status_enum.rb12
-rw-r--r--app/graphql/types/ci/pipeline_schedule_type.rb45
-rw-r--r--app/graphql/types/ci/runner_membership_filter_enum.rb7
-rw-r--r--app/graphql/types/environment_type.rb4
-rw-r--r--app/graphql/types/issue_type.rb19
-rw-r--r--app/graphql/types/merge_request_type.rb23
-rw-r--r--app/graphql/types/merge_requests/detailed_merge_status_enum.rb3
-rw-r--r--app/graphql/types/mutation_type.rb4
-rw-r--r--app/graphql/types/namespace/package_settings_type.rb49
-rw-r--r--app/graphql/types/notes/note_type.rb2
-rw-r--r--app/graphql/types/permission_types/ci/pipeline_schedules.rb17
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/graphql/types/projects/branch_rule_type.rb9
-rw-r--r--app/graphql/types/query_type.rb6
-rw-r--r--app/graphql/types/subscription_type.rb8
-rw-r--r--app/graphql/types/work_items/widget_interface.rb3
-rw-r--r--app/graphql/types/work_items/widgets/labels_update_input_type.rb20
-rw-r--r--app/helpers/application_helper.rb7
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/boards_helper.rb25
-rw-r--r--app/helpers/ci/pipeline_editor_helper.rb12
-rw-r--r--app/helpers/ci/pipelines_helper.rb3
-rw-r--r--app/helpers/compare_helper.rb3
-rw-r--r--app/helpers/events_helper.rb48
-rw-r--r--app/helpers/form_helper.rb23
-rw-r--r--app/helpers/groups_helper.rb7
-rw-r--r--app/helpers/hooks_helper.rb7
-rw-r--r--app/helpers/ide_helper.rb32
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/helpers/issues_helper.rb9
-rw-r--r--app/helpers/markup_helper.rb77
-rw-r--r--app/helpers/milestones_helper.rb26
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb16
-rw-r--r--app/helpers/recaptcha_helper.rb18
-rw-r--r--app/helpers/releases_helper.rb2
-rw-r--r--app/helpers/search_helper.rb34
-rw-r--r--app/helpers/selects_helper.rb5
-rw-r--r--app/helpers/sessions_helper.rb2
-rw-r--r--app/helpers/time_helper.rb6
-rw-r--r--app/helpers/timeboxes_helper.rb12
-rw-r--r--app/helpers/todos_helper.rb26
-rw-r--r--app/helpers/users/callouts_helper.rb4
-rw-r--r--app/helpers/users_helper.rb15
-rw-r--r--app/helpers/wiki_helper.rb10
-rw-r--r--app/mailers/emails/profile.rb24
-rw-r--r--app/mailers/previews/notify_preview.rb16
-rw-r--r--app/models/application_setting.rb46
-rw-r--r--app/models/application_setting_implementation.rb3
-rw-r--r--app/models/award_emoji.rb4
-rw-r--r--app/models/bulk_imports/entity.rb23
-rw-r--r--app/models/bulk_imports/export_status.rb14
-rw-r--r--app/models/bulk_imports/failure.rb20
-rw-r--r--app/models/bulk_imports/tracker.rb2
-rw-r--r--app/models/ci/build.rb49
-rw-r--r--app/models/ci/build_metadata.rb5
-rw-r--r--app/models/ci/job_token/project_scope_link.rb5
-rw-r--r--app/models/ci/job_token/scope.rb2
-rw-r--r--app/models/ci/pipeline.rb54
-rw-r--r--app/models/ci/pipeline_metadata.rb14
-rw-r--r--app/models/ci/runner.rb38
-rw-r--r--app/models/ci/secure_file.rb39
-rw-r--r--app/models/clusters/agents/implicit_authorization.rb2
-rw-r--r--app/models/concerns/approvable.rb4
-rw-r--r--app/models/concerns/atomic_internal_id.rb17
-rw-r--r--app/models/concerns/boards/listable.rb2
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/ci/metadatable.rb2
-rw-r--r--app/models/concerns/ci/partitionable.rb27
-rw-r--r--app/models/concerns/counter_attribute.rb63
-rw-r--r--app/models/concerns/has_wiki.rb2
-rw-r--r--app/models/concerns/integrations/base_data_fields.rb2
-rw-r--r--app/models/concerns/integrations/has_web_hook.rb2
-rw-r--r--app/models/concerns/issuable.rb44
-rw-r--r--app/models/concerns/participable.rb12
-rw-r--r--app/models/concerns/routable.rb3
-rw-r--r--app/models/concerns/timebox.rb92
-rw-r--r--app/models/deploy_key.rb1
-rw-r--r--app/models/deployment.rb21
-rw-r--r--app/models/diff_viewer/server_side.rb3
-rw-r--r--app/models/environment.rb18
-rw-r--r--app/models/event.rb10
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/group_group_link.rb2
-rw-r--r--app/models/group_label.rb1
-rw-r--r--app/models/hooks/project_hook.rb7
-rw-r--r--app/models/hooks/service_hook.rb2
-rw-r--r--app/models/hooks/web_hook.rb23
-rw-r--r--app/models/incident_management/timeline_event.rb8
-rw-r--r--app/models/incident_management/timeline_event_tag.rb20
-rw-r--r--app/models/incident_management/timeline_event_tag_link.rb11
-rw-r--r--app/models/integration.rb4
-rw-r--r--app/models/integrations/datadog.rb153
-rw-r--r--app/models/integrations/harbor.rb71
-rw-r--r--app/models/issue.rb18
-rw-r--r--app/models/iteration.rb6
-rw-r--r--app/models/jira_connect/public_key.rb48
-rw-r--r--app/models/jira_connect_installation.rb19
-rw-r--r--app/models/jira_import_state.rb2
-rw-r--r--app/models/label.rb8
-rw-r--r--app/models/member.rb3
-rw-r--r--app/models/members/member_role.rb11
-rw-r--r--app/models/merge_request.rb32
-rw-r--r--app/models/merge_request_diff_file.rb16
-rw-r--r--app/models/milestone.rb91
-rw-r--r--app/models/ml/candidate_param.rb1
-rw-r--r--app/models/ml/experiment.rb8
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/namespace/aggregation_schedule.rb13
-rw-r--r--app/models/namespace/detail.rb4
-rw-r--r--app/models/namespace/package_setting.rb6
-rw-r--r--app/models/note.rb9
-rw-r--r--app/models/notification_recipient.rb2
-rw-r--r--app/models/packages/package.rb7
-rw-r--r--app/models/packages/rpm/repository_file.rb25
-rw-r--r--app/models/pages/lookup_path.rb4
-rw-r--r--app/models/personal_access_token.rb9
-rw-r--r--app/models/preloaders/labels_preloader.rb2
-rw-r--r--app/models/preloaders/project_root_ancestor_preloader.rb3
-rw-r--r--app/models/project.rb57
-rw-r--r--app/models/project_authorization.rb39
-rw-r--r--app/models/project_ci_cd_setting.rb4
-rw-r--r--app/models/project_group_link.rb2
-rw-r--r--app/models/project_label.rb1
-rw-r--r--app/models/project_setting.rb1
-rw-r--r--app/models/project_statistics.rb73
-rw-r--r--app/models/projects/build_artifacts_size_refresh.rb4
-rw-r--r--app/models/protected_branch.rb4
-rw-r--r--app/models/protected_branch/merge_access_level.rb2
-rw-r--r--app/models/protected_branch/push_access_level.rb2
-rw-r--r--app/models/repository.rb40
-rw-r--r--app/models/resource_label_event.rb2
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/models/tree.rb5
-rw-r--r--app/models/user.rb91
-rw-r--r--app/models/user_detail.rb43
-rw-r--r--app/models/user_preference.rb2
-rw-r--r--app/models/users/banned_user.rb2
-rw-r--r--app/models/users/callout.rb3
-rw-r--r--app/models/users/namespace_callout.rb33
-rw-r--r--app/models/users/phone_number_validation.rb41
-rw-r--r--app/models/users/project_callout.rb6
-rw-r--r--app/models/users/user_follow_user.rb15
-rw-r--r--app/models/wiki.rb172
-rw-r--r--app/models/wiki_page.rb17
-rw-r--r--app/policies/ci/runner_policy.rb11
-rw-r--r--app/policies/group_label_policy.rb2
-rw-r--r--app/policies/group_policy.rb20
-rw-r--r--app/policies/issuable_policy.rb12
-rw-r--r--app/policies/issue_policy.rb4
-rw-r--r--app/policies/namespaces/user_namespace_policy.rb2
-rw-r--r--app/policies/note_policy.rb3
-rw-r--r--app/policies/project_label_policy.rb2
-rw-r--r--app/policies/project_policy.rb7
-rw-r--r--app/policies/todo_policy.rb2
-rw-r--r--app/presenters/ci/build_runner_presenter.rb4
-rw-r--r--app/presenters/ci/pipeline_presenter.rb2
-rw-r--r--app/presenters/deploy_key_presenter.rb9
-rw-r--r--app/presenters/event_presenter.rb2
-rw-r--r--app/presenters/key_presenter.rb22
-rw-r--r--app/serializers/board_serializer.rb5
-rw-r--r--app/serializers/board_simple_entity.rb8
-rw-r--r--app/serializers/current_board_entity.rb10
-rw-r--r--app/serializers/current_board_serializer.rb5
-rw-r--r--app/serializers/group_access_token_entity.rb2
-rw-r--r--app/serializers/import/github_org_entity.rb8
-rw-r--r--app/serializers/import/github_org_serializer.rb7
-rw-r--r--app/serializers/issue_entity.rb4
-rw-r--r--app/serializers/merge_request_noteable_entity.rb2
-rw-r--r--app/serializers/merge_request_user_entity.rb4
-rw-r--r--app/serializers/project_access_token_entity.rb2
-rw-r--r--app/serializers/user_serializer.rb2
-rw-r--r--app/services/admin/set_feature_flag_service.rb85
-rw-r--r--app/services/alert_management/create_alert_issue_service.rb2
-rw-r--r--app/services/authorized_project_update/project_recalculate_service.rb7
-rw-r--r--app/services/boards/issues/list_service.rb2
-rw-r--r--app/services/boards/lists/generate_service.rb39
-rw-r--r--app/services/boards/lists/list_service.rb20
-rw-r--r--app/services/bulk_imports/create_pipeline_trackers_service.rb6
-rw-r--r--app/services/bulk_imports/create_service.rb2
-rw-r--r--app/services/bulk_imports/repository_bundle_export_service.rb8
-rw-r--r--app/services/bulk_imports/uploads_export_service.rb5
-rw-r--r--app/services/ci/after_requeue_job_service.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb4
-rw-r--r--app/services/ci/generate_kubeconfig_service.rb12
-rw-r--r--app/services/ci/job_artifacts/delete_service.rb18
-rw-r--r--app/services/ci/parse_dotenv_artifact_service.rb2
-rw-r--r--app/services/ci/pipeline_artifacts/coverage_report_service.rb11
-rw-r--r--app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb11
-rw-r--r--app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb32
-rw-r--r--app/services/ci/process_build_service.rb2
-rw-r--r--app/services/ci/runners/register_runner_service.rb2
-rw-r--r--app/services/ci/unlock_artifacts_service.rb6
-rw-r--r--app/services/clusters/applications/destroy_service.rb23
-rw-r--r--app/services/clusters/applications/uninstall_service.rb29
-rw-r--r--app/services/concerns/users/participable_service.rb2
-rw-r--r--app/services/concerns/work_items/widgetable_service.rb12
-rw-r--r--app/services/google_cloud/enable_cloudsql_service.rb6
-rw-r--r--app/services/groups/import_export/import_service.rb2
-rw-r--r--app/services/import/github/cancel_project_import_service.rb36
-rw-r--r--app/services/import/github_service.rb28
-rw-r--r--app/services/incident_management/incidents/create_service.rb12
-rw-r--r--app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb2
-rw-r--r--app/services/incident_management/timeline_events/create_service.rb1
-rw-r--r--app/services/incident_management/timeline_events/destroy_service.rb2
-rw-r--r--app/services/incident_management/timeline_events/update_service.rb2
-rw-r--r--app/services/issuable/import_csv/base_service.rb2
-rw-r--r--app/services/issuable/process_assignees.rb10
-rw-r--r--app/services/issuable_base_service.rb47
-rw-r--r--app/services/issues/base_service.rb13
-rw-r--r--app/services/issues/clone_service.rb11
-rw-r--r--app/services/issues/create_service.rb23
-rw-r--r--app/services/issues/import_csv_service.rb4
-rw-r--r--app/services/issues/move_service.rb11
-rw-r--r--app/services/issues/update_service.rb10
-rw-r--r--app/services/jira_connect/create_asymmetric_jwt_service.rb51
-rw-r--r--app/services/labels/promote_service.rb4
-rw-r--r--app/services/members/create_service.rb4
-rw-r--r--app/services/members/destroy_service.rb25
-rw-r--r--app/services/merge_requests/approval_service.rb6
-rw-r--r--app/services/merge_requests/base_service.rb5
-rw-r--r--app/services/merge_requests/close_service.rb5
-rw-r--r--app/services/merge_requests/mark_reviewer_reviewed_service.rb2
-rw-r--r--app/services/merge_requests/merge_base_service.rb4
-rw-r--r--app/services/merge_requests/merge_service.rb24
-rw-r--r--app/services/merge_requests/mergeability/logger.rb16
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb14
-rw-r--r--app/services/merge_requests/request_review_service.rb1
-rw-r--r--app/services/merge_requests/update_assignees_service.rb12
-rw-r--r--app/services/merge_requests/update_service.rb17
-rw-r--r--app/services/ml/experiment_tracking/candidate_repository.rb85
-rw-r--r--app/services/ml/experiment_tracking/experiment_repository.rb30
-rw-r--r--app/services/namespaces/package_settings/update_service.rb8
-rw-r--r--app/services/notes/create_service.rb38
-rw-r--r--app/services/notification_service.rb15
-rw-r--r--app/services/onboarding/progress_service.rb2
-rw-r--r--app/services/packages/debian/create_package_file_service.rb14
-rw-r--r--app/services/packages/mark_packages_for_destruction_service.rb79
-rw-r--r--app/services/packages/rpm/parse_package_service.rb84
-rw-r--r--app/services/packages/rpm/repository_metadata/base_builder.rb30
-rw-r--r--app/services/packages/rpm/repository_metadata/build_primary_xml.rb73
-rw-r--r--app/services/packages/rpm/repository_metadata/build_repomd_xml.rb5
-rw-r--r--app/services/pages_domains/create_acme_order_service.rb10
-rw-r--r--app/services/pages_domains/create_service.rb34
-rw-r--r--app/services/pages_domains/delete_service.rb32
-rw-r--r--app/services/pages_domains/update_service.rb34
-rw-r--r--app/services/personal_access_tokens/revoke_service.rb3
-rw-r--r--app/services/projects/autocomplete_service.rb20
-rw-r--r--app/services/projects/blame_service.rb8
-rw-r--r--app/services/projects/container_repository/cleanup_tags_base_service.rb17
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb110
-rw-r--r--app/services/projects/container_repository/gitlab/cleanup_tags_service.rb3
-rw-r--r--app/services/projects/container_repository/third_party/cleanup_tags_service.rb106
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/projects/import_service.rb6
-rw-r--r--app/services/projects/update_service.rb40
-rw-r--r--app/services/releases/create_service.rb8
-rw-r--r--app/services/releases/destroy_service.rb4
-rw-r--r--app/services/releases/update_service.rb10
-rw-r--r--app/services/resource_access_tokens/create_service.rb13
-rw-r--r--app/services/search_service.rb10
-rw-r--r--app/services/users/build_service.rb2
-rw-r--r--app/services/users/dismiss_namespace_callout_service.rb11
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb4
-rw-r--r--app/services/web_hook_service.rb3
-rw-r--r--app/services/web_hooks/log_execution_service.rb2
-rw-r--r--app/services/work_items/create_service.rb12
-rw-r--r--app/services/work_items/update_service.rb21
-rw-r--r--app/services/work_items/widgets/base_service.rb5
-rw-r--r--app/services/work_items/widgets/labels_service/update_service.rb15
-rw-r--r--app/uploaders/job_artifact_uploader.rb1
-rw-r--r--app/uploaders/object_storage/cdn.rb10
-rw-r--r--app/uploaders/object_storage/cdn/google_cdn.rb4
-rw-r--r--app/uploaders/packages/rpm/repository_file_uploader.rb33
-rw-r--r--app/validators/json_schemas/build_metadata_secrets.json3
-rw-r--r--app/validators/json_schemas/ci_secure_file_metadata.json22
-rw-r--r--app/validators/json_schemas/merge_request_predictions_suggested_reviewers.json2
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml14
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml3
-rw-r--r--app/views/admin/application_settings/_eks.html.haml2
-rw-r--r--app/views/admin/application_settings/_email.html.haml2
-rw-r--r--app/views/admin/application_settings/_external_authorization_service_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_floc.html.haml2
-rw-r--r--app/views/admin/application_settings/_gitaly.html.haml4
-rw-r--r--app/views/admin/application_settings/_gitpod.html.haml2
-rw-r--r--app/views/admin/application_settings/_help_page.html.haml2
-rw-r--r--app/views/admin/application_settings/_import_export_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_ip_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_issue_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_jira_connect_application_key.html.haml2
-rw-r--r--app/views/admin/application_settings/_localization.html.haml2
-rw-r--r--app/views/admin/application_settings/_network_rate_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_note_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml2
-rw-r--r--app/views/admin/application_settings/_package_registry.html.haml8
-rw-r--r--app/views/admin/application_settings/_pages.html.haml2
-rw-r--r--app/views/admin/application_settings/_performance.html.haml2
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml2
-rw-r--r--app/views/admin/application_settings/_pipeline_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml2
-rw-r--r--app/views/admin/application_settings/_protected_paths.html.haml2
-rw-r--r--app/views/admin/application_settings/_registry.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml2
-rw-r--r--app/views/admin/application_settings/_sentry.html.haml2
-rw-r--r--app/views/admin/application_settings/_sidekiq_job_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_signin.html.haml2
-rw-r--r--app/views/admin/application_settings/_snowplow.html.haml2
-rw-r--r--app/views/admin/application_settings/_sourcegraph.html.haml2
-rw-r--r--app/views/admin/application_settings/_terms.html.haml2
-rw-r--r--app/views/admin/application_settings/_third_party_offers.html.haml2
-rw-r--r--app/views/admin/application_settings/_usage.html.haml2
-rw-r--r--app/views/admin/application_settings/_user_restrictions.html.haml6
-rw-r--r--app/views/admin/application_settings/_users_api_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_whats_new.html.haml2
-rw-r--r--app/views/admin/application_settings/appearances/preview_sign_in.html.haml4
-rw-r--r--app/views/admin/application_settings/ci_cd.html.haml19
-rw-r--r--app/views/admin/application_settings/general.html.haml4
-rw-r--r--app/views/admin/application_settings/service_usage_data.html.haml39
-rw-r--r--app/views/admin/applications/_form.html.haml2
-rw-r--r--app/views/admin/background_jobs/show.html.haml3
-rw-r--r--app/views/admin/broadcast_messages/_table.html.haml38
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml63
-rw-r--r--app/views/admin/dashboard/index.html.haml4
-rw-r--r--app/views/admin/deploy_keys/edit.html.haml4
-rw-r--r--app/views/admin/deploy_keys/new.html.haml7
-rw-r--r--app/views/admin/groups/_form.html.haml4
-rw-r--r--app/views/admin/groups/_group.html.haml3
-rw-r--r--app/views/admin/groups/show.html.haml177
-rw-r--r--app/views/admin/hooks/edit.html.haml2
-rw-r--r--app/views/admin/hooks/index.html.haml2
-rw-r--r--app/views/admin/impersonation_tokens/index.html.haml13
-rw-r--r--app/views/admin/projects/_projects.html.haml45
-rw-r--r--app/views/admin/projects/show.html.haml1
-rw-r--r--app/views/admin/users/_projects.html.haml24
-rw-r--r--app/views/admin/users/_user_detail_note.html.haml6
-rw-r--r--app/views/admin/users/projects.html.haml68
-rw-r--r--app/views/admin/users/show.html.haml220
-rw-r--r--app/views/award_emoji/_awards_block.html.haml2
-rw-r--r--app/views/ci/variables/_index.html.haml8
-rw-r--r--app/views/ci/variables/_url_query_variable_row.html.haml4
-rw-r--r--app/views/ci/variables/_variable_row.html.haml6
-rw-r--r--app/views/clusters/clusters/_health.html.haml4
-rw-r--r--app/views/clusters/clusters/index.html.haml1
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml2
-rw-r--r--app/views/devise/registrations/new.html.haml2
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml8
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml8
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml2
-rw-r--r--app/views/discussions/_discussion.html.haml22
-rw-r--r--app/views/events/event/_common.html.haml2
-rw-r--r--app/views/groups/_import_group_from_another_instance_panel.html.haml6
-rw-r--r--app/views/groups/_import_group_from_file_panel.html.haml4
-rw-r--r--app/views/groups/_personalize.html.haml2
-rw-r--r--app/views/groups/boards/index.html.haml2
-rw-r--r--app/views/groups/merge_requests.html.haml30
-rw-r--r--app/views/groups/milestones/_form.html.haml4
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/projects.html.haml71
-rw-r--r--app/views/groups/settings/_pages_settings.html.haml4
-rw-r--r--app/views/groups/settings/access_tokens/index.html.haml18
-rw-r--r--app/views/groups/settings/ci_cd/_form.html.haml4
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/groups/settings/integrations/index.html.haml9
-rw-r--r--app/views/groups/show.html.haml3
-rw-r--r--app/views/help/index.html.haml8
-rw-r--r--app/views/help/instance_configuration.html.haml2
-rw-r--r--app/views/help/show.html.haml2
-rw-r--r--app/views/import/_githubish_status.html.haml4
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml30
-rw-r--r--app/views/import/github/status.html.haml5
-rw-r--r--app/views/layouts/_flash.html.haml18
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/layouts/fullscreen.html.haml11
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/header/_gitlab_version.html.haml2
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml3
-rw-r--r--app/views/layouts/header/_new_dropdown.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml32
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml26
-rw-r--r--app/views/layouts/notify.html.haml2
-rw-r--r--app/views/layouts/terms.html.haml2
-rw-r--r--app/views/notify/access_token_revoked_email.html.haml7
-rw-r--r--app/views/notify/access_token_revoked_email.text.erb5
-rw-r--r--app/views/notify/project_was_exported_email.html.haml10
-rw-r--r--app/views/notify/project_was_moved_email.html.haml20
-rw-r--r--app/views/notify/project_was_not_exported_email.html.haml4
-rw-r--r--app/views/notify/repository_push_email.text.haml2
-rw-r--r--app/views/notify/request_review_merge_request_email.html.haml2
-rw-r--r--app/views/notify/send_unsubscribed_notification.html.haml2
-rw-r--r--app/views/notify/two_factor_otp_attempt_failed_email.html.haml51
-rw-r--r--app/views/notify/two_factor_otp_attempt_failed_email.text.haml7
-rw-r--r--app/views/notify/unknown_sign_in_email.html.haml2
-rw-r--r--app/views/notify/unknown_sign_in_email.text.haml2
-rw-r--r--app/views/profiles/active_sessions/index.html.haml2
-rw-r--r--app/views/profiles/audit_log.html.haml2
-rw-r--r--app/views/profiles/chat_names/index.html.haml2
-rw-r--r--app/views/profiles/emails/index.html.haml6
-rw-r--r--app/views/profiles/gpg_keys/_form.html.haml4
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml3
-rw-r--r--app/views/profiles/keys/_form.html.haml4
-rw-r--r--app/views/profiles/keys/_key_details.html.haml51
-rw-r--r--app/views/profiles/keys/index.html.haml3
-rw-r--r--app/views/profiles/notifications/show.html.haml2
-rw-r--r--app/views/profiles/passwords/edit.html.haml9
-rw-r--r--app/views/profiles/passwords/new.html.haml4
-rw-r--r--app/views/profiles/preferences/show.html.haml4
-rw-r--r--app/views/profiles/show.html.haml51
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml8
-rw-r--r--app/views/projects/_fork_suggestion.html.haml5
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml2
-rw-r--r--app/views/projects/_transfer.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml1
-rw-r--r--app/views/projects/artifacts/file.html.haml1
-rw-r--r--app/views/projects/blame/show.html.haml3
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml16
-rw-r--r--app/views/projects/blob/_pipeline_tour_success.html.haml2
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml12
-rw-r--r--app/views/projects/blob/preview.html.haml2
-rw-r--r--app/views/projects/blob/show.html.haml1
-rw-r--r--app/views/projects/boards/index.html.haml2
-rw-r--r--app/views/projects/branch_rules/_show.html.haml2
-rw-r--r--app/views/projects/buttons/_clone.html.haml8
-rw-r--r--app/views/projects/buttons/_download_links.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml2
-rw-r--r--app/views/projects/buttons/_star.html.haml14
-rw-r--r--app/views/projects/cleanup/_show.html.haml4
-rw-r--r--app/views/projects/cluster_agents/show.html.haml1
-rw-r--r--app/views/projects/commit/_signature.html.haml2
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml4
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/default_branch/_show.html.haml4
-rw-r--r--app/views/projects/deploy_keys/edit.html.haml2
-rw-r--r--app/views/projects/deployments/_deployment.html.haml2
-rw-r--r--app/views/projects/deployments/_rollback.haml7
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/environments/metrics.html.haml2
-rw-r--r--app/views/projects/find_file/show.html.haml1
-rw-r--r--app/views/projects/graphs/charts.html.haml1
-rw-r--r--app/views/projects/hooks/edit.html.haml2
-rw-r--r--app/views/projects/hooks/index.html.haml4
-rw-r--r--app/views/projects/incidents/show.html.haml1
-rw-r--r--app/views/projects/issues/_discussion.html.haml3
-rw-r--r--app/views/projects/issues/_issue.html.haml5
-rw-r--r--app/views/projects/issues/_work_item_links.html.haml2
-rw-r--r--app/views/projects/issues/service_desk/_service_desk_info_content.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml1
-rw-r--r--app/views/projects/jobs/_user.html.haml2
-rw-r--r--app/views/projects/merge_requests/_awards_block.html.haml5
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml5
-rw-r--r--app/views/projects/merge_requests/_discussion_filter.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml11
-rw-r--r--app/views/projects/merge_requests/_nav_btns.html.haml7
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml1
-rw-r--r--app/views/projects/merge_requests/show.html.haml6
-rw-r--r--app/views/projects/milestones/_form.html.haml2
-rw-r--r--app/views/projects/mirrors/_authentication_method.html.haml10
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-rw-r--r--app/views/projects/network/_head.html.haml2
-rw-r--r--app/views/projects/pages/_destroy.haml18
-rw-r--r--app/views/projects/pages/new.html.haml11
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml6
-rw-r--r--app/views/projects/pipeline_schedules/edit.html.haml5
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml34
-rw-r--r--app/views/projects/pipeline_schedules/new.html.haml5
-rw-r--r--app/views/projects/pipelines/_info.html.haml13
-rw-r--r--app/views/projects/pipelines/new.html.haml1
-rw-r--r--app/views/projects/project_templates/_project_fields_form.html.haml2
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_create_protected_branch.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_dropdown.html.haml8
-rw-r--r--app/views/projects/protected_tags/shared/_create_protected_tag.html.haml8
-rw-r--r--app/views/projects/releases/index.html.haml1
-rw-r--r--app/views/projects/releases/show.html.haml1
-rw-r--r--app/views/projects/repositories/_feed.html.haml18
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml2
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml18
-rw-r--r--app/views/projects/settings/branch_rules/index.html.haml6
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_badge.html.haml18
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml3
-rw-r--r--app/views/projects/settings/integrations/index.html.haml11
-rw-r--r--app/views/projects/settings/merge_requests/show.html.haml5
-rw-r--r--app/views/projects/show.html.haml1
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/starrers/_starrer.html.haml4
-rw-r--r--app/views/projects/tags/new.html.haml20
-rw-r--r--app/views/projects/tree/show.html.haml1
-rw-r--r--app/views/projects/triggers/_index.html.haml18
-rw-r--r--app/views/projects/work_items/index.html.haml2
-rw-r--r--app/views/registrations/welcome/show.html.haml6
-rw-r--r--app/views/search/_results.html.haml37
-rw-r--r--app/views/search/_results_list.html.haml18
-rw-r--r--app/views/search/_results_status.html.haml26
-rw-r--r--app/views/search/_results_status_horiz_nav.html.haml22
-rw-r--r--app/views/search/_results_status_vert_nav.html.haml23
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/search/results/_wiki_blob.html.haml2
-rw-r--r--app/views/search/show.html.haml3
-rw-r--r--app/views/shared/_clone_panel.html.haml4
-rw-r--r--app/views/shared/_commit_well.html.haml4
-rw-r--r--app/views/shared/_custom_attributes.html.haml19
-rw-r--r--app/views/shared/_file_highlight.html.haml3
-rw-r--r--app/views/shared/access_tokens/_form.html.haml5
-rw-r--r--app/views/shared/blob/_markdown_buttons.html.haml20
-rw-r--r--app/views/shared/deploy_keys/_project_group_form.html.haml2
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml2
-rw-r--r--app/views/shared/deploy_tokens/_index.html.haml20
-rw-r--r--app/views/shared/doorkeeper/applications/_index.html.haml2
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml12
-rw-r--r--app/views/shared/issuable/_feed_buttons.html.haml8
-rw-r--r--app/views/shared/issuable/_form.html.haml4
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml4
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml4
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml9
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_reviewers.html.haml5
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/issue_type/_details_content.html.haml6
-rw-r--r--app/views/shared/issue_type/_details_header.html.haml7
-rw-r--r--app/views/shared/issue_type/_emoji_block.html.haml4
-rw-r--r--app/views/shared/labels/_form.html.haml6
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/members/_requests.html.haml2
-rw-r--r--app/views/shared/milestones/_issuables.html.haml28
-rw-r--r--app/views/shared/milestones/_participants_tab.html.haml8
-rw-r--r--app/views/shared/notes/_note.html.haml15
-rw-r--r--app/views/shared/projects/_project.html.haml12
-rw-r--r--app/views/shared/projects/_search_form.html.haml2
-rw-r--r--app/views/shared/runners/_runner_description.html.haml12
-rw-r--r--app/views/shared/runners/_shared_runners_description.html.haml7
-rw-r--r--app/views/shared/snippets/_snippet.html.haml25
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml3
-rw-r--r--app/views/shared/users/_user.html.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml13
-rw-r--r--app/views/shared/web_hooks/_hook_errors.html.haml12
-rw-r--r--app/views/shared/web_hooks/_index.html.haml25
-rw-r--r--app/views/shared/wikis/pages.html.haml4
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/workers/all_queues.yml126
-rw-r--r--app/workers/bulk_import_worker.rb5
-rw-r--r--app/workers/bulk_imports/entity_worker.rb26
-rw-r--r--app/workers/bulk_imports/export_request_worker.rb89
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb54
-rw-r--r--app/workers/ci/cancel_pipeline_worker.rb1
-rw-r--r--app/workers/ci/parse_secure_file_metadata_worker.rb15
-rw-r--r--app/workers/ci/pipeline_success_unlock_artifacts_worker.rb4
-rw-r--r--app/workers/clusters/applications/uninstall_worker.rb6
-rw-r--r--app/workers/concerns/application_worker.rb10
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb34
-rw-r--r--app/workers/concerns/gitlab/github_import/stage_methods.rb4
-rw-r--r--app/workers/experiments/record_conversion_event_worker.rb22
-rw-r--r--app/workers/gitlab/github_import/attachments/import_issue_worker.rb23
-rw-r--r--app/workers/gitlab/github_import/attachments/import_merge_request_worker.rb23
-rw-r--r--app/workers/gitlab/github_import/attachments/import_note_worker.rb23
-rw-r--r--app/workers/gitlab/github_import/attachments/import_release_worker.rb23
-rw-r--r--app/workers/gitlab/github_import/import_issue_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/import_release_attachments_worker.rb6
-rw-r--r--app/workers/gitlab/github_import/stage/import_attachments_worker.rb9
-rw-r--r--app/workers/gitlab/github_import/stage/import_issue_events_worker.rb14
-rw-r--r--app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_notes_worker.rb2
-rw-r--r--app/workers/gitlab/github_import/stage/import_repository_worker.rb18
-rw-r--r--app/workers/incident_management/pager_duty/process_incident_worker.rb2
-rw-r--r--app/workers/incident_management/process_alert_worker_v2.rb4
-rw-r--r--app/workers/merge_requests/delete_source_branch_worker.rb6
-rw-r--r--app/workers/onboarding/issue_created_worker.rb (renamed from app/workers/namespaces/onboarding_issue_created_worker.rb)7
-rw-r--r--app/workers/onboarding/pipeline_created_worker.rb (renamed from app/workers/namespaces/onboarding_pipeline_created_worker.rb)7
-rw-r--r--app/workers/onboarding/progress_worker.rb (renamed from app/workers/namespaces/onboarding_progress_worker.rb)7
-rw-r--r--app/workers/onboarding/user_added_worker.rb (renamed from app/workers/namespaces/onboarding_user_added_worker.rb)7
-rw-r--r--app/workers/process_commit_worker.rb24
1382 files changed, 16680 insertions, 9921 deletions
diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
index 461b2dad479..57a237c3e84 100644
--- a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
+++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
@@ -45,16 +45,34 @@ export default {
'initialActiveAccessTokens',
'noActiveTokensMessage',
'showRole',
+ 'information',
],
data() {
return {
- activeAccessTokens: this.initialActiveAccessTokens,
+ activeAccessTokens: convertObjectPropsToCamelCase(this.initialActiveAccessTokens, {
+ deep: true,
+ }),
currentPage: INITIAL_PAGE,
};
},
computed: {
filteredFields() {
- return this.showRole ? FIELDS : FIELDS.filter((field) => field.key !== 'role');
+ const ignoredFields = [];
+
+ // Show 'action' column only when there are no active tokens or when some of them have a revokePath
+ const showAction =
+ this.activeAccessTokens.length === 0 ||
+ this.activeAccessTokens.some((token) => token.revokePath);
+
+ if (!showAction) {
+ ignoredFields.push('action');
+ }
+
+ if (!this.showRole) {
+ ignoredFields.push('role');
+ }
+
+ return FIELDS.filter(({ key }) => !ignoredFields.includes(key));
},
header() {
return sprintf(this.$options.i18n.header, {
@@ -100,6 +118,10 @@ export default {
<hr />
<h5>{{ header }}</h5>
+ <p v-if="information" data-testid="information-section">
+ {{ information }}
+ </p>
+
<gl-table
data-testid="active-tokens"
:empty-text="noActiveTokensMessage"
diff --git a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
index 6b52bd84656..ce5342ad1ea 100644
--- a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
+++ b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
@@ -2,10 +2,13 @@
import { GlAlert } from '@gitlab/ui';
import { createAlert, VARIANT_INFO } from '~/flash';
import { __, n__, sprintf } from '~/locale';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
import { EVENT_ERROR, EVENT_SUCCESS, FORM_SELECTOR } from './constants';
+const convertEventDetail = (event) => convertObjectPropsToCamelCase(event.detail, { deep: true });
+
export default {
EVENT_ERROR,
EVENT_SUCCESS,
@@ -54,8 +57,8 @@ export default {
/** @type {HTMLFormElement} */
this.form = document.querySelector(FORM_SELECTOR);
- /** @type {HTMLInputElement} */
- this.submitButton = this.form.querySelector('input[type=submit]');
+ /** @type {HTMLButtonElement} */
+ this.submitButton = this.form.querySelector('[type=submit]');
},
methods: {
beforeDisplayResults() {
@@ -68,20 +71,21 @@ export default {
onError(event) {
this.beforeDisplayResults();
- const [{ errors }] = event.detail;
+ const [{ errors }] = convertEventDetail(event);
this.errors = errors;
this.submitButton.classList.remove('disabled');
+ this.submitButton.removeAttribute('disabled');
},
onSuccess(event) {
this.beforeDisplayResults();
- const [{ new_token: newToken }] = event.detail;
+ const [{ newToken }] = convertEventDetail(event);
this.newToken = newToken;
this.infoAlert = createAlert({ message: this.alertInfoMessage, variant: VARIANT_INFO });
- // Selectively reset all input fields except for the date picker and submit.
+ // Selectively reset all input fields except for the date picker.
// The form token creation is not controlled by Vue.
this.form.querySelectorAll('input[type=text]:not([id$=expires_at])').forEach((el) => {
el.value = '';
diff --git a/app/assets/javascripts/access_tokens/components/tokens_app.vue b/app/assets/javascripts/access_tokens/components/tokens_app.vue
index 10d4d62d803..1f72f5e19e2 100644
--- a/app/assets/javascripts/access_tokens/components/tokens_app.vue
+++ b/app/assets/javascripts/access_tokens/components/tokens_app.vue
@@ -79,7 +79,7 @@ export default {
</script>
<template>
- <div>
+ <div class="js-search-settings-section">
<token
v-for="(tokenData, tokenType) in enabledTokenTypes"
:key="tokenType"
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
index f0c1b415157..510f118bbb5 100644
--- a/app/assets/javascripts/access_tokens/index.js
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -20,6 +20,7 @@ export const initAccessTokenTableApp = () => {
const {
accessTokenType,
accessTokenTypePlural,
+ information,
initialActiveAccessTokens: initialActiveAccessTokensJson,
noActiveTokensMessage: noTokensMessage,
} = el.dataset;
@@ -30,12 +31,7 @@ export const initAccessTokenTableApp = () => {
sprintf(__('This user has no active %{accessTokenTypePlural}.'), { accessTokenTypePlural });
const showRole = 'showRole' in el.dataset;
- const initialActiveAccessTokens = convertObjectPropsToCamelCase(
- JSON.parse(initialActiveAccessTokensJson),
- {
- deep: true,
- },
- );
+ const initialActiveAccessTokens = JSON.parse(initialActiveAccessTokensJson);
return new Vue({
el,
@@ -43,6 +39,7 @@ export const initAccessTokenTableApp = () => {
provide: {
accessTokenType,
accessTokenTypePlural,
+ information,
initialActiveAccessTokens,
noActiveTokensMessage,
showRole,
@@ -103,7 +100,7 @@ export const initNewAccessTokenApp = () => {
export const initTokensApp = () => {
const el = document.getElementById('js-tokens-app');
- if (!el) return false;
+ if (!el) return null;
const tokensData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.tokensData), {
deep: true,
diff --git a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
index 8ad218ab97b..a41ff42df20 100644
--- a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
+++ b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
@@ -2,7 +2,7 @@
import { GlModal, GlTabs, GlTab, GlSearchBoxByType, GlSprintf, GlBadge } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
@@ -193,7 +193,7 @@ export default {
window.location.reload();
}
if (!values[0] && !values[1]) {
- createFlash({
+ createAlert({
message: s__(
'ContextCommits|Failed to create/remove context commits. Please try again.',
),
diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js
index 4e5a2c7b371..d4c9db2fa33 100644
--- a/app/assets/javascripts/add_context_commits_modal/store/actions.js
+++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js
@@ -1,6 +1,6 @@
import _ from 'lodash';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@@ -71,7 +71,7 @@ export const createContextCommits = ({ state }, { commits, forceReload = false }
})
.catch(() => {
if (forceReload) {
- createFlash({
+ createAlert({
message: s__('ContextCommits|Failed to create context commits. Please try again.'),
});
}
@@ -113,7 +113,7 @@ export const removeContextCommits = ({ state }, forceReload = false) =>
})
.catch(() => {
if (forceReload) {
- createFlash({
+ createAlert({
message: s__('ContextCommits|Failed to delete context commits. Please try again.'),
});
}
diff --git a/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
index 7f6e5dc4f35..80c216024a0 100644
--- a/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
+++ b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
@@ -44,7 +44,7 @@ export default {
:items="databases"
right
:toggle-text="selectedDatabase"
- aria-labelledby="label"
+ toggle-aria-labelled-by="label"
@select="selectDatabase"
/>
</div>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/base.vue b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
new file mode 100644
index 00000000000..b7bafe46327
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
@@ -0,0 +1,112 @@
+<script>
+import { GlPagination } from '@gitlab/ui';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
+import { createAlert, VARIANT_DANGER } from '~/flash';
+import { s__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import MessagesTable from './messages_table.vue';
+
+const PER_PAGE = 20;
+
+export default {
+ name: 'BroadcastMessagesBase',
+ components: {
+ GlPagination,
+ MessagesTable,
+ },
+
+ props: {
+ page: {
+ type: Number,
+ required: true,
+ },
+ messagesCount: {
+ type: Number,
+ required: true,
+ },
+ messages: {
+ type: Array,
+ required: true,
+ },
+ },
+
+ i18n: {
+ deleteError: s__(
+ 'BroadcastMessages|There was an issue deleting this message, please try again later.',
+ ),
+ },
+
+ data() {
+ return {
+ currentPage: this.page,
+ totalMessages: this.messagesCount,
+ visibleMessages: this.messages.map((message) => ({
+ ...message,
+ disable_delete: false,
+ })),
+ };
+ },
+
+ computed: {
+ hasVisibleMessages() {
+ return this.visibleMessages.length > 0;
+ },
+ },
+
+ watch: {
+ totalMessages(newVal, oldVal) {
+ // Pagination controls disappear when there is only
+ // one page worth of messages. Since we're relying on static data,
+ // this could hide messages on the next page, or leave the user
+ // stranded on page 2 when deleting the last message.
+ // Force a page reload to avoid this edge case.
+ if (newVal === PER_PAGE && oldVal === PER_PAGE + 1) {
+ redirectTo(this.buildPageUrl(1));
+ }
+ },
+ },
+
+ methods: {
+ buildPageUrl(newPage) {
+ return buildUrlWithCurrentLocation(`?page=${newPage}`);
+ },
+
+ async deleteMessage(messageId) {
+ const index = this.visibleMessages.findIndex((m) => m.id === messageId);
+ if (!index === -1) return;
+
+ const message = this.visibleMessages[index];
+ this.$set(this.visibleMessages, index, { ...message, disable_delete: true });
+
+ try {
+ await axios.delete(message.delete_path);
+ } catch (e) {
+ this.$set(this.visibleMessages, index, { ...message, disable_delete: false });
+ createAlert({ message: this.$options.i18n.deleteError, variant: VARIANT_DANGER });
+ return;
+ }
+
+ // Remove the message from the table
+ this.visibleMessages = this.visibleMessages.filter((m) => m.id !== messageId);
+ this.totalMessages -= 1;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <messages-table
+ v-if="hasVisibleMessages"
+ :messages="visibleMessages"
+ @delete-message="deleteMessage"
+ />
+ <gl-pagination
+ v-model="currentPage"
+ :total-items="totalMessages"
+ :link-gen="buildPageUrl"
+ align="center"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
new file mode 100644
index 00000000000..1408312d3e4
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
@@ -0,0 +1,113 @@
+<script>
+import { GlButton, GlTableLite, GlSafeHtmlDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+const DEFAULT_TD_CLASSES = 'gl-vertical-align-middle!';
+
+export default {
+ name: 'MessagesTable',
+ components: {
+ GlButton,
+ GlTableLite,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ i18n: {
+ edit: __('Edit'),
+ delete: __('Delete'),
+ },
+ props: {
+ messages: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ fields() {
+ if (this.glFeatures.roleTargetedBroadcastMessages) return this.$options.allFields;
+ return this.$options.allFields.filter((f) => f.key !== 'target_roles');
+ },
+ },
+ allFields: [
+ {
+ key: 'status',
+ label: __('Status'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'preview',
+ label: __('Preview'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'starts_at',
+ label: __('Starts'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'ends_at',
+ label: __('Ends'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'target_roles',
+ label: __('Target roles'),
+ tdClass: DEFAULT_TD_CLASSES,
+ thAttr: { 'data-testid': 'target-roles-th' },
+ },
+ {
+ key: 'target_path',
+ label: __('Target Path'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'type',
+ label: __('Type'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'buttons',
+ label: '',
+ tdClass: `${DEFAULT_TD_CLASSES} gl-white-space-nowrap`,
+ },
+ ],
+ safeHtmlConfig: {
+ ADD_TAGS: ['use'],
+ },
+};
+</script>
+<template>
+ <gl-table-lite
+ :items="messages"
+ :fields="fields"
+ :tbody-tr-attr="{ 'data-testid': 'message-row' }"
+ stacked="md"
+ >
+ <template #cell(preview)="{ item: { preview } }">
+ <div v-safe-html:[$options.safeHtmlConfig]="preview"></div>
+ </template>
+
+ <template #cell(buttons)="{ item: { id, edit_path, disable_delete } }">
+ <gl-button
+ icon="pencil"
+ :aria-label="$options.i18n.edit"
+ :href="edit_path"
+ data-testid="edit-message"
+ />
+
+ <gl-button
+ class="gl-ml-3"
+ icon="remove"
+ variant="danger"
+ :aria-label="$options.i18n.delete"
+ rel="nofollow"
+ :disabled="disable_delete"
+ :data-testid="`delete-message-${id}`"
+ @click="$emit('delete-message', id)"
+ />
+ </template>
+ </gl-table-lite>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/index.js b/app/assets/javascripts/admin/broadcast_messages/index.js
new file mode 100644
index 00000000000..81952d2033e
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/index.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import BroadcastMessagesBase from './components/base.vue';
+
+export default () => {
+ const el = document.querySelector('#js-broadcast-messages');
+ const { page, messagesCount, messages } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'BroadcastMessages',
+ render(createElement) {
+ return createElement(BroadcastMessagesBase, {
+ props: {
+ page: Number(page),
+ messagesCount: Number(messagesCount),
+ messages: JSON.parse(messages),
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/deploy_keys/components/table.vue b/app/assets/javascripts/admin/deploy_keys/components/table.vue
index 6b140590938..be85ee43891 100644
--- a/app/assets/javascripts/admin/deploy_keys/components/table.vue
+++ b/app/assets/javascripts/admin/deploy_keys/components/table.vue
@@ -5,7 +5,7 @@ import { __ } from '~/locale';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import csrf from '~/lib/utils/csrf';
export default {
@@ -151,7 +151,7 @@ export default {
}),
);
} catch (error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.apiErrorMessage,
captureError: true,
error,
diff --git a/app/assets/javascripts/admin/statistics_panel/store/actions.js b/app/assets/javascripts/admin/statistics_panel/store/actions.js
index 77782cdc187..4f952698d7a 100644
--- a/app/assets/javascripts/admin/statistics_panel/store/actions.js
+++ b/app/assets/javascripts/admin/statistics_panel/store/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@@ -21,7 +21,7 @@ export const receiveStatisticsSuccess = ({ commit }, statistics) =>
export const receiveStatisticsError = ({ commit }, error) => {
commit(types.RECEIVE_STATISTICS_ERROR, error);
- createFlash({
+ createAlert({
message: s__('AdminDashboard|Error loading the statistics. Please try again'),
});
};
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue
index b4b84594276..f569cda0a4b 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/admin/users/components/users_table.vue
@@ -1,6 +1,6 @@
<script>
import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
import { thWidthPercent } from '~/lib/utils/table_utility';
import { s__, __ } from '~/locale';
@@ -50,7 +50,7 @@ export default {
}, {});
},
error(error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.groupCountFetchError,
captureError: true,
error,
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index 37a6ea16018..c0cac958a42 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -216,8 +216,11 @@ export default {
this.pagination = initialPaginationState;
this.sort = sortObjectToString({ sortBy, sortDesc });
},
+ showAlertLink({ iid }) {
+ return joinPaths(window.location.pathname, iid, 'details');
+ },
navigateToAlertDetails({ iid }, index, { metaKey }) {
- return visitUrl(joinPaths(window.location.pathname, iid, 'details'), metaKey);
+ return visitUrl(this.showAlertLink({ iid }), metaKey);
},
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
@@ -357,7 +360,7 @@ export default {
:title="`${item.iid} - ${item.title}`"
data-testid="idField"
>
- #{{ item.iid }} {{ item.title }}
+ <gl-link :href="showAlertLink(item)"> #{{ item.iid }} {{ item.title }} </gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index 1f970ef1846..bf456b6adaa 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -2,7 +2,7 @@
import { GlButton, GlAlert, GlTabs, GlTab } from '@gitlab/ui';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import httpStatusCodes from '~/lib/utils/http_status';
import { typeSet, i18n, tabIndices } from '../constants';
@@ -75,7 +75,7 @@ export default {
return nodes;
},
error(err) {
- createFlash({ message: err });
+ createAlert({ message: err });
},
},
currentIntegration: {
@@ -125,7 +125,7 @@ export default {
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0];
if (error) {
- createFlash({ message: error });
+ createAlert({ message: error });
return;
}
@@ -140,7 +140,7 @@ export default {
}
})
.catch(() => {
- createFlash({ message: ADD_INTEGRATION_ERROR });
+ createAlert({ message: ADD_INTEGRATION_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -161,7 +161,7 @@ export default {
.then(({ data: { httpIntegrationUpdate, prometheusIntegrationUpdate } = {} } = {}) => {
const error = httpIntegrationUpdate?.errors[0] || prometheusIntegrationUpdate?.errors[0];
if (error) {
- createFlash({ message: error });
+ createAlert({ message: error });
return;
}
@@ -174,13 +174,13 @@ export default {
this.clearCurrentIntegration({ type });
}
- createFlash({
+ createAlert({
message: this.$options.i18n.changesSaved,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
})
.catch(() => {
- createFlash({ message: UPDATE_INTEGRATION_ERROR });
+ createAlert({ message: UPDATE_INTEGRATION_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -198,7 +198,7 @@ export default {
const [error] =
httpIntegrationResetToken?.errors || prometheusIntegrationResetToken.errors;
if (error) {
- return createFlash({ message: error });
+ return createAlert({ message: error });
}
const integration =
@@ -212,14 +212,14 @@ export default {
variables: integration,
});
- return createFlash({
+ return createAlert({
message: this.$options.i18n.changesSaved,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
},
)
.catch(() => {
- createFlash({ message: RESET_INTEGRATION_TOKEN_ERROR });
+ createAlert({ message: RESET_INTEGRATION_TOKEN_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -252,7 +252,7 @@ export default {
);
},
error() {
- createFlash({ message: DEFAULT_ERROR });
+ createAlert({ message: DEFAULT_ERROR });
},
});
} else {
@@ -272,7 +272,7 @@ export default {
this.tabIndex = tabIndex;
})
.catch(() => {
- createFlash({ message: DEFAULT_ERROR });
+ createAlert({ message: DEFAULT_ERROR });
});
},
deleteIntegration({ id, type }) {
@@ -290,16 +290,16 @@ export default {
.then(({ data: { httpIntegrationDestroy } = {} } = {}) => {
const error = httpIntegrationDestroy?.errors[0];
if (error) {
- return createFlash({ message: error });
+ return createAlert({ message: error });
}
this.clearCurrentIntegration({ type });
- return createFlash({
+ return createAlert({
message: this.$options.i18n.integrationRemoved,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
})
.catch(() => {
- createFlash({ message: DELETE_INTEGRATION_ERROR });
+ createAlert({ message: DELETE_INTEGRATION_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -320,9 +320,9 @@ export default {
return service
.updateTestAlert(payload)
.then(() => {
- return createFlash({
+ return createAlert({
message: this.$options.i18n.alertSent,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
})
.catch((error) => {
@@ -330,7 +330,7 @@ export default {
if (error.response?.status === httpStatusCodes.FORBIDDEN) {
message = INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR;
}
- createFlash({ message });
+ createAlert({ message });
});
},
saveAndTestAlertPayload(integration, payload) {
diff --git a/app/assets/javascripts/alerts_settings/utils/cache_updates.js b/app/assets/javascripts/alerts_settings/utils/cache_updates.js
index a50b6515afa..2e64312b0e0 100644
--- a/app/assets/javascripts/alerts_settings/utils/cache_updates.js
+++ b/app/assets/javascripts/alerts_settings/utils/cache_updates.js
@@ -1,5 +1,5 @@
import produce from 'immer';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { DELETE_INTEGRATION_ERROR, ADD_INTEGRATION_ERROR } from './error_messages';
@@ -59,7 +59,7 @@ const addIntegrationToStore = (
};
const onError = (data, message) => {
- createFlash({ message });
+ createAlert({ message });
throw new Error(data.errors);
};
diff --git a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
index 567e534d9cf..ffb61230661 100644
--- a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
+++ b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
@@ -1,7 +1,7 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { flatten, isEqual, keyBy } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { sprintf, s__ } from '~/locale';
import { METRICS_POPOVER_CONTENT } from '../constants';
import { removeFlash, prepareTimeMetricsData } from '../utils';
@@ -17,7 +17,7 @@ const requestData = ({ request, endpoint, path, params, name }) => {
),
{ requestTypeName: name },
);
- createFlash({ message });
+ createAlert({ message });
});
};
diff --git a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
index 457a52d3807..5651789e2c7 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
@@ -1,7 +1,7 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { number } from '~/lib/utils/unit_format';
import { __, s__ } from '~/locale';
import usageTrendsCountQuery from '../graphql/queries/usage_trends_count.query.graphql';
@@ -35,7 +35,7 @@ export default {
});
},
error(error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.loadCountsError,
captureError: true,
error,
diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js
index 667aa878261..5b5abbdf50b 100644
--- a/app/assets/javascripts/api/projects_api.js
+++ b/app/assets/javascripts/api/projects_api.js
@@ -5,6 +5,7 @@ import { buildApiUrl } from './api_utils';
const PROJECTS_PATH = '/api/:version/projects.json';
const PROJECT_IMPORT_MEMBERS_PATH = '/api/:version/projects/:id/import_project_members/:project_id';
const PROJECT_REPOSITORY_SIZE_PATH = '/api/:version/projects/:id/repository_size';
+const PROJECT_TRANSFER_LOCATIONS_PATH = 'api/:version/projects/:id/transfer_locations';
export function getProjects(query, options, callback = () => {}) {
const url = buildApiUrl(PROJECTS_PATH);
@@ -42,3 +43,10 @@ export function updateRepositorySize(projectPath) {
);
return axios.post(url);
}
+
+export const getTransferLocations = (projectId, params = {}) => {
+ const url = buildApiUrl(PROJECT_TRANSFER_LOCATIONS_PATH).replace(':id', projectId);
+ const defaultParams = { per_page: DEFAULT_PER_PAGE };
+
+ return axios.get(url, { params: { ...defaultParams, ...params } });
+};
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js
index c743b18d572..369abe95d49 100644
--- a/app/assets/javascripts/api/user_api.js
+++ b/app/assets/javascripts/api/user_api.js
@@ -1,5 +1,5 @@
import { DEFAULT_PER_PAGE } from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
@@ -55,7 +55,7 @@ export function getUserProjects(userId, query, options, callback) {
})
.then(({ data }) => callback(data))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching projects'),
}),
);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index a3ffb4df7b7..9ab1d6bfd80 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -7,7 +7,7 @@ import { getEmojiScoreWithIntent } from '~/emoji/utils';
import { getCookie, setCookie, scrollToElement } from '~/lib/utils/common_utils';
import * as Emoji from '~/emoji';
import { dispose, fixTitle } from '~/tooltips';
-import createFlash from './flash';
+import { createAlert } from '~/flash';
import axios from './lib/utils/axios_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { __ } from './locale';
@@ -491,7 +491,7 @@ export class AwardsHandler {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong on our end.'),
}),
);
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index d1570e16639..f68666f8a0c 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -2,7 +2,7 @@
import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import { escape, debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { s__, sprintf } from '~/locale';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
@@ -136,14 +136,14 @@ export default {
if (this.isEditing) {
return this.saveBadge()
.then(() => {
- createFlash({
+ createAlert({
message: s__('Badges|Badge saved.'),
- type: 'notice',
+ variant: VARIANT_INFO,
});
this.wasValidated = false;
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__(
'Badges|Saving the badge failed, please check the entered URLs and try again.',
),
@@ -154,14 +154,14 @@ export default {
return this.addBadge()
.then(() => {
- createFlash({
+ createAlert({
message: s__('Badges|New badge added.'),
- type: 'notice',
+ variant: VARIANT_INFO,
});
this.wasValidated = false;
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__(
'Badges|Adding the badge failed, please check the entered URLs and try again.',
),
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
index 0303930de5d..a7a21d65475 100644
--- a/app/assets/javascripts/badges/components/badge_settings.vue
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf, GlModal } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { __, s__ } from '~/locale';
import Badge from './badge.vue';
import BadgeForm from './badge_form.vue';
@@ -40,13 +40,13 @@ export default {
onSubmitModal() {
this.deleteBadge(this.badgeInModal)
.then(() => {
- createFlash({
+ createAlert({
message: s__('Badges|The badge was deleted.'),
- type: 'notice',
+ variant: VARIANT_INFO,
});
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__('Badges|Deleting the badge failed, please try again.'),
});
throw error;
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
index 2b0aaa74e83..feac6f10b1e 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
@@ -1,5 +1,5 @@
import { isEmpty } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { scrollToElement } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { CHANGES_TAB, DISCUSSION_TAB, SHOW_TAB } from '../../../constants';
@@ -18,7 +18,7 @@ export const addDraftToDiscussion = ({ commit }, { endpoint, data }) =>
return res;
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('An error occurred adding a draft to the thread.'),
});
});
@@ -32,7 +32,7 @@ export const createNewDraft = ({ commit }, { endpoint, data }) =>
return res;
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('An error occurred adding a new draft.'),
});
});
@@ -44,7 +44,7 @@ export const deleteDraft = ({ commit, getters }, draft) =>
commit(types.DELETE_DRAFT, draft.id);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while deleting the comment'),
}),
);
@@ -62,7 +62,7 @@ export const fetchDrafts = ({ commit, getters, state, dispatch }) =>
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while fetching pending comments'),
}),
);
@@ -122,7 +122,7 @@ export const updateDraft = (
.then((data) => commit(types.RECEIVE_DRAFT_UPDATE_SUCCESS, data))
.then(callback)
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while updating the comment'),
}),
);
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js
index df5214ea7ab..75e4ae63c18 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js
@@ -22,7 +22,11 @@ export const draftsPerFileHashAndLine = (state) =>
acc[draft.file_hash] = {};
}
- acc[draft.file_hash][draft.line_code] = draft;
+ if (!acc[draft.file_hash][draft.line_code]) {
+ acc[draft.file_hash][draft.line_code] = [];
+ }
+
+ acc[draft.file_hash][draft.line_code].push(draft);
}
return acc;
@@ -61,18 +65,15 @@ export const shouldRenderDraftRowInDiscussion = (state, getters) => (discussionI
export const draftForDiscussion = (state, getters) => (discussionId) =>
getters.draftsPerDiscussionId[discussionId] || {};
-export const draftForLine = (state, getters) => (diffFileSha, line, side = null) => {
+export const draftsForLine = (state, getters) => (diffFileSha, line, side = null) => {
const draftsForFile = getters.draftsPerFileHashAndLine[diffFileSha];
-
const key = side !== null ? parallelLineKey(line, side) : line.line_code;
+ const showDraftsForThisSide = showDraftOnSide(line, side);
- if (draftsForFile) {
- const draft = draftsForFile[key];
- if (draft && showDraftOnSide(line, side)) {
- return draft;
- }
+ if (showDraftsForThisSide && draftsForFile?.[key]) {
+ return draftsForFile[key];
}
- return {};
+ return [];
};
export const draftsForFile = (state) => (diffFileSha) =>
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 679940d1317..68f5180cc03 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names */
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -80,7 +80,7 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
success(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while fetching Markdown preview'),
}),
);
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index 23b66405844..3239375bf7c 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -145,6 +145,20 @@ export const LINK_TEXT = {
customizable: false,
};
+export const INDENT_LINE = {
+ id: 'editing.indentLine',
+ description: __('Indent line'),
+ defaultKeys: ['mod+]'], // eslint-disable-line @gitlab/require-i18n-strings
+ customizable: false,
+};
+
+export const OUTDENT_LINE = {
+ id: 'editing.outdentLine',
+ description: __('Outdent line'),
+ defaultKeys: ['mod+['], // eslint-disable-line @gitlab/require-i18n-strings
+ customizable: false,
+};
+
export const TOGGLE_MARKDOWN_PREVIEW = {
id: 'editing.toggleMarkdownPreview',
description: __('Toggle Markdown preview'),
diff --git a/app/assets/javascripts/blame/blame_redirect.js b/app/assets/javascripts/blame/blame_redirect.js
new file mode 100644
index 00000000000..155e2a3a2cd
--- /dev/null
+++ b/app/assets/javascripts/blame/blame_redirect.js
@@ -0,0 +1,23 @@
+import { setUrlParams } from '~/lib/utils/url_utility';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
+
+export default function redirectToCorrectBlamePage() {
+ const { hash } = window.location;
+ const linesPerPage = parseInt(document.querySelector('.js-per-page').dataset.perPage, 10);
+ const params = new URLSearchParams(window.location.search);
+ const currentPage = parseInt(params.get('page'), 10);
+ const isPaginationDisabled = params.get('no_pagination');
+ if (hash && linesPerPage && !isPaginationDisabled) {
+ const lineNumber = parseInt(hash.split('#L')[1], 10);
+ const pageToRedirect = Math.ceil(lineNumber / linesPerPage);
+ const isRedirectNeeded =
+ (pageToRedirect > 1 && pageToRedirect !== currentPage) || pageToRedirect < currentPage;
+ if (isRedirectNeeded) {
+ createAlert({
+ message: __('Please wait a few moments while we load the file history for this line.'),
+ });
+ window.location.href = setUrlParams({ page: pageToRedirect });
+ }
+ }
+}
diff --git a/app/assets/javascripts/blob/3d_viewer/index.js b/app/assets/javascripts/blob/3d_viewer/index.js
index 2831c37838b..4fbc9044cf0 100644
--- a/app/assets/javascripts/blob/3d_viewer/index.js
+++ b/app/assets/javascripts/blob/3d_viewer/index.js
@@ -1,6 +1,6 @@
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader';
-import * as THREE from 'three/build/three.module';
+import * as THREE from 'three';
import MeshObject from './mesh_object';
export default class Renderer {
diff --git a/app/assets/javascripts/blob/3d_viewer/mesh_object.js b/app/assets/javascripts/blob/3d_viewer/mesh_object.js
index 5322dc00e86..6c816b2d07f 100644
--- a/app/assets/javascripts/blob/3d_viewer/mesh_object.js
+++ b/app/assets/javascripts/blob/3d_viewer/mesh_object.js
@@ -1,4 +1,4 @@
-import { Matrix4, MeshLambertMaterial, Mesh } from 'three/build/three.module';
+import { Matrix4, MeshLambertMaterial, Mesh } from 'three';
const defaultColor = 0xe24329;
const materials = {
diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
index 0a5bcf326a1..df38c5400e2 100644
--- a/app/assets/javascripts/blob/blob_line_permalink_updater.js
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -22,6 +22,7 @@ const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
};
function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, elementsToUpdate) {
+ if (!blobContentHolder) return;
const updateBlameAndBlobPermalinkCb = () => {
// Wait for the hash to update from the LineHighlighter callback
setTimeout(() => {
diff --git a/app/assets/javascripts/blob/components/table_contents.vue b/app/assets/javascripts/blob/components/table_contents.vue
index 07da262ec9a..b3410b94b98 100644
--- a/app/assets/javascripts/blob/components/table_contents.vue
+++ b/app/assets/javascripts/blob/components/table_contents.vue
@@ -26,6 +26,8 @@ export default {
} else if (blobViewerAttr('data-loaded') === 'true') {
this.isHidden = false;
this.generateHeaders();
+
+ this.observer.disconnect();
}
});
@@ -47,13 +49,11 @@ export default {
if (headers.length) {
const firstHeader = getHeaderNumber(headers[0]);
- headers.forEach((el) => {
- this.items.push({
- text: el.textContent.trim(),
- anchor: el.querySelector('a').getAttribute('id'),
- spacing: Math.max((getHeaderNumber(el) - firstHeader) * 8, 0),
- });
- });
+ this.items = headers.map((el) => ({
+ text: el.textContent.trim(),
+ anchor: el.querySelector('a').getAttribute('id'),
+ spacing: Math.max((getHeaderNumber(el) - firstHeader) * 8, 0),
+ }));
}
},
},
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index 991f98c89e7..adc2649e5df 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import Api from '~/api';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
@@ -155,7 +155,7 @@ export default class FileTemplateMediator {
}
})
.catch((err) =>
- createFlash({
+ createAlert({
message: __(`An error occurred while fetching the template: ${err}`),
}),
);
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index 4c497db9842..44b75cc3e68 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -1,5 +1,5 @@
import { SwaggerUIBundle } from 'swagger-ui-dist';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
export default () => {
@@ -15,7 +15,7 @@ export default () => {
});
})
.catch((error) => {
- createFlash({
+ createAlert({
message: __('Something went wrong while initializing the OpenAPI viewer'),
});
throw error;
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 5ca3f131d99..8d323c335d3 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import {
REPO_BLOB_LOAD_VIEWER_START,
@@ -69,7 +69,7 @@ export const handleBlobRichViewer = (viewer, type) => {
loadRichBlobViewer(type)
.then((module) => module?.default(viewer))
.catch((error) => {
- createFlash({
+ createAlert({
message: __('Error loading file viewer.'),
});
throw error;
@@ -221,7 +221,7 @@ export class BlobViewer {
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error loading viewer'),
}),
);
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index d73e1cc43b0..1c9c99dcc2f 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
import BlobFileDropzone from '../blob/blob_file_dropzone';
@@ -79,7 +79,7 @@ export default () => {
initPopovers();
})
.catch((e) =>
- createFlash({
+ createAlert({
message: e,
}),
);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 2ee2e199358..78e3f934183 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -3,7 +3,7 @@ import { SourceEditorExtension } from '~/editor/extensions/source_editor_extensi
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
import { insertFinalNewline } from '~/lib/utils/text_utility';
@@ -44,7 +44,7 @@ export default class EditBlob {
},
]);
} catch (e) {
- createFlash({
+ createAlert({
message: `${BLOB_EDITOR_ERROR}: ${e}`,
});
}
@@ -130,7 +130,7 @@ export default class EditBlob {
currentPane.renderGFM();
})
.catch(() =>
- createFlash({
+ createAlert({
message: BLOB_PREVIEW_ERROR,
}),
);
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 92a623d65d4..3a2b11a649d 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -17,9 +17,9 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/toolt
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
+import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import { ListType } from '../constants';
import eventHub from '../eventhub';
-import BoardBlockedIcon from './board_blocked_icon.vue';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
@@ -34,7 +34,7 @@ export default {
IssueDueDate,
IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
- BoardBlockedIcon,
+ IssuableBlockedIcon,
GlSprintf,
BoardCardMoveToPosition,
WorkItemTypeIcon,
@@ -218,7 +218,7 @@ export default {
<div>
<div class="gl-display-flex" dir="auto">
<h4 class="board-card-title gl-mb-0 gl-mt-0 gl-mr-3 gl-font-base gl-overflow-break-word">
- <board-blocked-icon
+ <issuable-blocked-icon
v-if="item.blocked"
:item="item"
:unique-id="`${item.id}${list.id}`"
@@ -250,7 +250,8 @@ export default {
>{{ item.title }}</a
>
</h4>
- <board-card-move-to-position :item="item" :list="list" :index="index" />
+ <!-- TODO: remove the condition when https://gitlab.com/gitlab-org/gitlab/-/issues/377862 is resolved -->
+ <board-card-move-to-position v-if="!isEpicBoard" :item="item" :list="list" :index="index" />
</div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
<template v-for="label in orderedLabels">
@@ -397,7 +398,6 @@ export default {
:img-size="avatarSize"
class="js-no-trigger user-avatar-link"
tooltip-placement="bottom"
- :enforce-gl-avatar="true"
>
<span class="js-assignee-tooltip">
<span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span>
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index fa0c798ca9d..11a5d89cc8c 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -8,6 +8,7 @@ import { __ } from '~/locale';
import {
FILTERED_SEARCH_TERM,
FILTER_ANY,
+ TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { AssigneeFilterType } from '~/boards/constants';
@@ -55,6 +56,7 @@ export default {
myReactionEmoji,
releaseTag,
confidential,
+ healthStatus,
} = this.filterParams;
const filteredSearchValue = [];
@@ -154,6 +156,13 @@ export default {
});
}
+ if (healthStatus) {
+ filteredSearchValue.push({
+ type: TOKEN_TYPE_HEALTH,
+ value: { data: healthStatus, operator: '=' },
+ });
+ }
+
if (this.filterParams['not[authorUsername]']) {
filteredSearchValue.push({
type: 'author',
@@ -248,6 +257,7 @@ export default {
iterationCadenceId,
releaseTag,
confidential,
+ healthStatus,
} = this.filterParams;
let iteration = iterationId;
let cadence = iterationCadenceId;
@@ -292,6 +302,7 @@ export default {
my_reaction_emoji: myReactionEmoji,
release_tag: releaseTag,
confidential,
+ [TOKEN_TYPE_HEALTH]: healthStatus,
},
(value) => {
if (value || value === false) {
@@ -390,6 +401,9 @@ export default {
case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data);
break;
+ case TOKEN_TYPE_HEALTH:
+ filterParams.healthStatus = filter.value.data;
+ break;
default:
break;
}
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 9f359a25234..eb889344c1e 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -4,7 +4,6 @@ import { mapGetters, mapActions, mapState } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { visitUrl, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { formType } from '../constants';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
@@ -45,7 +44,6 @@ export default {
BoardConfigurationOptions,
GlAlert,
},
- mixins: [glFeatureFlagMixin()],
inject: {
fullPath: {
default: '',
@@ -233,9 +231,8 @@ export default {
}
},
setIteration(iteration) {
- if (this.glFeatures.iterationCadences) {
- this.board.iterationCadenceId = iteration.iterationCadenceId;
- }
+ this.board.iterationCadenceId = iteration.iterationCadenceId;
+
this.$set(this.board, 'iteration', {
id: iteration.id,
});
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index ed22a375271..91b7f5004ad 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -2,8 +2,6 @@ import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql
import { __ } from '~/locale';
import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
-import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql';
-import boardBlockingEpicsQuery from './graphql/board_blocking_epics.query.graphql';
import destroyBoardListMutation from './graphql/board_list_destroy.mutation.graphql';
import updateBoardListMutation from './graphql/board_list_update.mutation.graphql';
@@ -67,15 +65,6 @@ export const listsQuery = {
},
};
-export const blockingIssuablesQueries = {
- [issuableTypes.issue]: {
- query: boardBlockingIssuesQuery,
- },
- [issuableTypes.epic]: {
- query: boardBlockingEpicsQuery,
- },
-};
-
export const updateListQueries = {
[issuableTypes.issue]: {
mutation: updateBoardListMutation,
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 854717ed4c4..a7003edba47 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -86,6 +86,7 @@ function mountBoardApp(el) {
milestoneListsAvailable: parseBoolean(el.dataset.milestoneListsAvailable),
assigneeListsAvailable: parseBoolean(el.dataset.assigneeListsAvailable),
iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable),
+ healthStatusFeatureAvailable: parseBoolean(el.dataset.healthStatusFeatureAvailable),
allowScopedLabels: parseBoolean(el.dataset.scopedLabels),
swimlanesFeatureAvailable: gon.licensed_features?.swimlanes,
multipleIssueBoardsAvailable: parseBoolean(el.dataset.multipleBoardsAvailable),
diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js
index 17fd3939441..d05b53f1a50 100644
--- a/app/assets/javascripts/branches/divergence_graph.js
+++ b/app/assets/javascripts/branches/divergence_graph.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import DivergenceGraph from './components/divergence_graph.vue';
@@ -51,7 +51,7 @@ export default (endpoint, defaultBranch) => {
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error fetching diverging counts for branches. Please try again.'),
}),
);
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
index fea4b56153f..1f8096da94d 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
@@ -1,5 +1,13 @@
<script>
-import { GlTable, GlButton, GlBadge, GlTooltipDirective, GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlAvatar,
+ GlAvatarLink,
+ GlBadge,
+ GlButton,
+ GlTable,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -15,14 +23,15 @@ export default {
),
},
components: {
- GlTable,
- GlButton,
- GlBadge,
ClipboardButton,
- TooltipOnTruncate,
- GlAvatarLink,
+ GlAlert,
GlAvatar,
+ GlAvatarLink,
+ GlBadge,
+ GlButton,
+ GlTable,
TimeAgoTooltip,
+ TooltipOnTruncate,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -138,13 +147,15 @@ export default {
/>
</template>
</gl-table>
- <div
+ <gl-alert
v-else
+ variant="warning"
+ :dismissible="false"
+ :show-icon="false"
data-testid="no_triggers_content"
data-qa-selector="no_triggers_content"
- class="settings-message gl-text-center gl-mb-3"
>
{{ s__('Pipelines|No triggers have been created yet. Add one using the form above.') }}
- </div>
+ </gl-alert>
</div>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
index 59ddf4b19d8..8d891ff1746 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
@@ -1,5 +1,7 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
+import { reportMessageToSentry } from '../utils';
import getAdminVariables from '../graphql/queries/variables.query.graphql';
import {
ADD_MUTATION_ACTION,
@@ -21,7 +23,11 @@ export default {
data() {
return {
adminVariables: [],
+ hasNextPage: false,
isInitialLoading: true,
+ isLoadingMoreItems: false,
+ loadingCounter: 0,
+ pageInfo: {},
};
},
apollo: {
@@ -30,8 +36,29 @@ export default {
update(data) {
return data?.ciVariables?.nodes || [];
},
+ result({ data }) {
+ this.pageInfo = data?.ciVariables?.pageInfo || this.pageInfo;
+ this.hasNextPage = this.pageInfo?.hasNextPage || false;
+
+ // Because graphQL has a limit of 100 items,
+ // we batch load all the variables by making successive queries
+ // to keep the same UX. As a safeguard, we make sure that we cannot go over
+ // 20 consecutive API calls, which means 2000 variables loaded maximum.
+ if (!this.hasNextPage) {
+ this.isLoadingMoreItems = false;
+ } else if (this.loadingCounter < 20) {
+ this.hasNextPage = false;
+ this.fetchMoreVariables();
+ this.loadingCounter += 1;
+ } else {
+ createAlert({ message: this.$options.tooManyCallsError });
+ reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
+ }
+ },
error() {
- createFlash({ message: variableFetchErrorText });
+ this.isLoadingMoreItems = false;
+ this.hasNextPage = false;
+ createAlert({ message: variableFetchErrorText });
},
watchLoading(flag) {
if (!flag) {
@@ -42,7 +69,10 @@ export default {
},
computed: {
isLoading() {
- return this.$apollo.queries.adminVariables.loading && this.isInitialLoading;
+ return (
+ (this.$apollo.queries.adminVariables.loading && this.isInitialLoading) ||
+ this.isLoadingMoreItems
+ );
},
},
methods: {
@@ -52,6 +82,15 @@ export default {
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
+ fetchMoreVariables() {
+ this.isLoadingMoreItems = true;
+
+ this.$apollo.queries.adminVariables.fetchMore({
+ variables: {
+ after: this.pageInfo.endCursor,
+ },
+ });
+ },
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
@@ -66,10 +105,9 @@ export default {
},
});
- const { errors } = data[currentMutation.name];
-
- if (errors.length > 0) {
- createFlash({ message: errors[0] });
+ if (data[currentMutation.name]?.errors?.length) {
+ const { errors } = data[currentMutation.name];
+ createAlert({ message: errors[0] });
} else {
// The writing to cache for admin variable is not working
// because there is no ID in the cache at the top level.
@@ -77,10 +115,14 @@ export default {
this.$apollo.queries.adminVariables.refetch();
}
} catch {
- createFlash({ message: genericMutationErrorText });
+ createAlert({ message: genericMutationErrorText });
}
},
},
+ componentName: 'InstanceVariables',
+ i18n: {
+ tooManyCallsError: __('Maximum number of variables loaded (2000)'),
+ },
mutationData: {
[ADD_MUTATION_ACTION]: { action: addAdminVariable, name: 'addAdminVariable' },
[UPDATE_MUTATION_ACTION]: { action: updateAdminVariable, name: 'updateAdminVariable' },
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
index 3522243e3e7..4af696b8dab 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
@@ -1,7 +1,9 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { reportMessageToSentry } from '../utils';
import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import {
ADD_MUTATION_ACTION,
@@ -25,6 +27,10 @@ export default {
data() {
return {
groupVariables: [],
+ hasNextPage: false,
+ isLoadingMoreItems: false,
+ loadingCounter: 0,
+ pageInfo: {},
};
},
apollo: {
@@ -38,8 +44,28 @@ export default {
update(data) {
return data?.group?.ciVariables?.nodes || [];
},
+ result({ data }) {
+ this.pageInfo = data?.group?.ciVariables?.pageInfo || this.pageInfo;
+ this.hasNextPage = this.pageInfo?.hasNextPage || false;
+ // Because graphQL has a limit of 100 items,
+ // we batch load all the variables by making successive queries
+ // to keep the same UX. As a safeguard, we make sure that we cannot go over
+ // 20 consecutive API calls, which means 2000 variables loaded maximum.
+ if (!this.hasNextPage) {
+ this.isLoadingMoreItems = false;
+ } else if (this.loadingCounter < 20) {
+ this.hasNextPage = false;
+ this.fetchMoreVariables();
+ this.loadingCounter += 1;
+ } else {
+ createAlert({ message: this.$options.tooManyCallsError });
+ reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
+ }
+ },
error() {
- createFlash({ message: variableFetchErrorText });
+ this.isLoadingMoreItems = false;
+ this.hasNextPage = false;
+ createAlert({ message: variableFetchErrorText });
},
},
},
@@ -48,7 +74,7 @@ export default {
return this.glFeatures.groupScopedCiVariables;
},
isLoading() {
- return this.$apollo.queries.groupVariables.loading;
+ return this.$apollo.queries.groupVariables.loading || this.isLoadingMoreItems;
},
},
methods: {
@@ -58,6 +84,16 @@ export default {
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
+ fetchMoreVariables() {
+ this.isLoadingMoreItems = true;
+
+ this.$apollo.queries.groupVariables.fetchMore({
+ variables: {
+ fullPath: this.groupPath,
+ after: this.pageInfo.endCursor,
+ },
+ });
+ },
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
@@ -74,16 +110,19 @@ export default {
},
});
- const { errors } = data[currentMutation.name];
-
- if (errors.length > 0) {
- createFlash({ message: errors[0] });
+ if (data[currentMutation.name]?.errors?.length) {
+ const { errors } = data[currentMutation.name];
+ createAlert({ message: errors[0] });
}
} catch {
- createFlash({ message: genericMutationErrorText });
+ createAlert({ message: genericMutationErrorText });
}
},
},
+ componentName: 'GroupVariables',
+ i18n: {
+ tooManyCallsError: __('Maximum number of variables loaded (2000)'),
+ },
mutationData: {
[ADD_MUTATION_ACTION]: { action: addGroupVariable, name: 'addGroupVariable' },
[UPDATE_MUTATION_ACTION]: { action: updateGroupVariable, name: 'updateGroupVariable' },
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
index 29db02a3c59..6bd549817f8 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
@@ -1,9 +1,10 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import getProjectEnvironments from '../graphql/queries/project_environments.query.graphql';
import getProjectVariables from '../graphql/queries/project_variables.query.graphql';
-import { mapEnvironmentNames } from '../utils';
+import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
@@ -25,6 +26,10 @@ export default {
inject: ['endpoint', 'projectFullPath', 'projectId'],
data() {
return {
+ hasNextPage: false,
+ isLoadingMoreItems: false,
+ loadingCounter: 0,
+ pageInfo: {},
projectEnvironments: [],
projectVariables: [],
};
@@ -41,21 +46,42 @@ export default {
return mapEnvironmentNames(data?.project?.environments?.nodes);
},
error() {
- createFlash({ message: environmentFetchErrorText });
+ createAlert({ message: environmentFetchErrorText });
},
},
projectVariables: {
query: getProjectVariables,
variables() {
return {
+ after: null,
fullPath: this.projectFullPath,
};
},
update(data) {
return data?.project?.ciVariables?.nodes || [];
},
+ result({ data }) {
+ this.pageInfo = data?.project?.ciVariables?.pageInfo || this.pageInfo;
+ this.hasNextPage = this.pageInfo?.hasNextPage || false;
+ // Because graphQL has a limit of 100 items,
+ // we batch load all the variables by making successive queries
+ // to keep the same UX. As a safeguard, we make sure that we cannot go over
+ // 20 consecutive API calls, which means 2000 variables loaded maximum.
+ if (!this.hasNextPage) {
+ this.isLoadingMoreItems = false;
+ } else if (this.loadingCounter < 20) {
+ this.hasNextPage = false;
+ this.fetchMoreVariables();
+ this.loadingCounter += 1;
+ } else {
+ createAlert({ message: this.$options.tooManyCallsError });
+ reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
+ }
+ },
error() {
- createFlash({ message: variableFetchErrorText });
+ this.isLoadingMoreItems = false;
+ this.hasNextPage = false;
+ createAlert({ message: variableFetchErrorText });
},
},
},
@@ -63,7 +89,8 @@ export default {
isLoading() {
return (
this.$apollo.queries.projectVariables.loading ||
- this.$apollo.queries.projectEnvironments.loading
+ this.$apollo.queries.projectEnvironments.loading ||
+ this.isLoadingMoreItems
);
},
},
@@ -74,6 +101,16 @@ export default {
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
+ fetchMoreVariables() {
+ this.isLoadingMoreItems = true;
+
+ this.$apollo.queries.projectVariables.fetchMore({
+ variables: {
+ fullPath: this.projectFullPath,
+ after: this.pageInfo.endCursor,
+ },
+ });
+ },
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
@@ -89,16 +126,19 @@ export default {
variable,
},
});
-
- const { errors } = data[currentMutation.name];
- if (errors.length > 0) {
- createFlash({ message: errors[0] });
+ if (data[currentMutation.name]?.errors?.length) {
+ const { errors } = data[currentMutation.name];
+ createAlert({ message: errors[0] });
}
- } catch (e) {
- createFlash({ message: genericMutationErrorText });
+ } catch {
+ createAlert({ message: genericMutationErrorText });
}
},
},
+ componentName: 'ProjectVariables',
+ i18n: {
+ tooManyCallsError: __('Maximum number of variables loaded (2000)'),
+ },
mutationData: {
[ADD_MUTATION_ACTION]: { action: addProjectVariable, name: 'addProjectVariable' },
[UPDATE_MUTATION_ACTION]: { action: updateProjectVariable, name: 'updateProjectVariable' },
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index 1bb94080694..959ef6864fb 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -75,8 +75,7 @@ export default {
props: {
isLoading: {
type: Boolean,
- required: false,
- default: false,
+ required: true,
},
variables: {
type: Array,
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
index 9acc9fbffb6..f1fe188348d 100644
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
+++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
@@ -9,10 +9,10 @@ export default {
LegacyCiVariableTable,
},
computed: {
- ...mapState(['isGroup']),
+ ...mapState(['isGroup', 'isProject']),
},
mounted() {
- if (!this.isGroup) {
+ if (this.isProject) {
this.fetchEnvironments();
}
},
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index e2dd28cdaa1..ccad08ef8b6 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -52,7 +52,7 @@ export const groupString = 'Group';
// eslint-disable-next-line @gitlab/require-i18n-strings
export const instanceString = 'Instance';
// eslint-disable-next-line @gitlab/require-i18n-strings
-export const projectString = 'Instance';
+export const projectString = 'Project';
export const AWS_TIP_DISMISSED_COOKIE_NAME = 'ci_variable_list_constants_aws_tip_dismissed';
export const AWS_TIP_MESSAGE = __(
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
index c6dd6d4faaf..b5555fe4401 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
@@ -1,9 +1,13 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-query getGroupVariables($fullPath: ID!) {
+query getGroupVariables($after: String, $first: Int = 100, $fullPath: ID!) {
group(fullPath: $fullPath) {
id
- ciVariables {
+ ciVariables(after: $after, first: $first) {
+ pageInfo {
+ ...PageInfo
+ }
nodes {
...BaseCiVariable
... on CiGroupVariable {
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
index a60a50e4bc4..08b5bf7af16 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
@@ -1,9 +1,13 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-query getProjectVariables($fullPath: ID!) {
+query getProjectVariables($after: String, $first: Int = 100, $fullPath: ID!) {
project(fullPath: $fullPath) {
id
- ciVariables {
+ ciVariables(after: $after, first: $first) {
+ pageInfo {
+ ...PageInfo
+ }
nodes {
...BaseCiVariable
environmentScope
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
index 95056842b49..2667d6606fe 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
@@ -1,7 +1,11 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-query getVariables {
- ciVariables {
+query getVariables($after: String, $first: Int = 100) {
+ ciVariables(after: $after, first: $first) {
+ pageInfo {
+ ...PageInfo
+ }
nodes {
...BaseCiVariable
... on CiInstanceVariable {
diff --git a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js b/app/assets/javascripts/ci_variable_list/graphql/settings.js
index c041531ae30..ecdc4f220bd 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js
+++ b/app/assets/javascripts/ci_variable_list/graphql/settings.js
@@ -3,7 +3,7 @@ import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
} from '../../lib/utils/common_utils';
-import { getIdFromGraphQLId } from '../../graphql_shared/utils';
+import { convertToGraphQLId, getIdFromGraphQLId } from '../../graphql_shared/utils';
import {
GRAPHQL_GROUP_TYPE,
GRAPHQL_PROJECT_TYPE,
@@ -30,6 +30,7 @@ const mapVariableTypes = (variables = [], kind) => {
return {
__typename: `Ci${kind}Variable`,
...convertObjectPropsToCamelCase(ciVar),
+ id: convertToGraphQLId('Ci::Variable', ciVar.id),
variableType: ciVar.variable_type ? ciVar.variable_type.toUpperCase() : ciVar.variableType,
};
});
@@ -40,9 +41,16 @@ const prepareProjectGraphQLResponse = ({ data, projectId, errors = [] }) => {
errors,
project: {
__typename: GRAPHQL_PROJECT_TYPE,
- id: projectId,
+ id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, projectId),
ciVariables: {
- __typename: 'CiVariableConnection',
+ __typename: `Ci${GRAPHQL_PROJECT_TYPE}VariableConnection`,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
nodes: mapVariableTypes(data.variables, projectString),
},
},
@@ -54,9 +62,16 @@ const prepareGroupGraphQLResponse = ({ data, groupId, errors = [] }) => {
errors,
group: {
__typename: GRAPHQL_GROUP_TYPE,
- id: groupId,
+ id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, groupId),
ciVariables: {
- __typename: 'CiVariableConnection',
+ __typename: `Ci${GRAPHQL_GROUP_TYPE}VariableConnection`,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
nodes: mapVariableTypes(data.variables, groupString),
},
},
@@ -68,24 +83,42 @@ const prepareAdminGraphQLResponse = ({ data, errors = [] }) => {
errors,
ciVariables: {
__typename: `Ci${instanceString}VariableConnection`,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
nodes: mapVariableTypes(data.variables, instanceString),
},
};
};
-const callProjectEndpoint = async ({
+async function callProjectEndpoint({
endpoint,
fullPath,
variable,
projectId,
cache,
destroy = false,
-}) => {
+}) {
try {
const { data } = await axios.patch(endpoint, {
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
- return prepareProjectGraphQLResponse({ data, projectId });
+
+ const graphqlData = prepareProjectGraphQLResponse({ data, projectId });
+
+ cache.writeQuery({
+ query: getProjectVariables,
+ variables: {
+ fullPath,
+ after: null,
+ },
+ data: graphqlData,
+ });
+ return graphqlData;
} catch (e) {
return prepareProjectGraphQLResponse({
data: cache.readQuery({ query: getProjectVariables, variables: { fullPath } }),
@@ -93,7 +126,7 @@ const callProjectEndpoint = async ({
errors: [...e.response.data],
});
}
-};
+}
const callGroupEndpoint = async ({
endpoint,
@@ -107,7 +140,15 @@ const callGroupEndpoint = async ({
const { data } = await axios.patch(endpoint, {
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
- return prepareGroupGraphQLResponse({ data, groupId });
+
+ const graphqlData = prepareGroupGraphQLResponse({ data, groupId });
+
+ cache.writeQuery({
+ query: getGroupVariables,
+ data: graphqlData,
+ });
+
+ return graphqlData;
} catch (e) {
return prepareGroupGraphQLResponse({
data: cache.readQuery({ query: getGroupVariables, variables: { fullPath } }),
@@ -123,7 +164,14 @@ const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false })
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
- return prepareAdminGraphQLResponse({ data });
+ const graphqlData = prepareAdminGraphQLResponse({ data });
+
+ cache.writeQuery({
+ query: getAdminVariables,
+ data: graphqlData,
+ });
+
+ return graphqlData;
} catch (e) {
return prepareAdminGraphQLResponse({
data: cache.readQuery({ query: getAdminVariables }),
@@ -163,3 +211,46 @@ export const resolvers = {
},
},
};
+
+export const mergeVariables = (existing, incoming, { args }) => {
+ if (!existing || !args?.after) {
+ return incoming;
+ }
+
+ const { nodes, ...rest } = incoming;
+ const result = rest;
+ result.nodes = [...existing.nodes, ...nodes];
+
+ return result;
+};
+
+export const cacheConfig = {
+ cacheConfig: {
+ typePolicies: {
+ Query: {
+ fields: {
+ ciVariables: {
+ keyArgs: false,
+ merge: mergeVariables,
+ },
+ },
+ },
+ Project: {
+ fields: {
+ ciVariables: {
+ keyArgs: ['fullPath', 'endpoint', 'projectId'],
+ merge: mergeVariables,
+ },
+ },
+ },
+ Group: {
+ fields: {
+ ciVariables: {
+ keyArgs: ['fullPath'],
+ merge: mergeVariables,
+ },
+ },
+ },
+ },
+ },
+};
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index f5bdd4c7b1e..1b69da9e086 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -6,7 +6,7 @@ import CiAdminVariables from './components/ci_admin_variables.vue';
import CiGroupVariables from './components/ci_group_variables.vue';
import CiProjectVariables from './components/ci_project_variables.vue';
import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
-import { resolvers } from './graphql/resolvers';
+import { cacheConfig, resolvers } from './graphql/settings';
import createStore from './store';
const mountCiVariableListApp = (containerEl) => {
@@ -45,7 +45,7 @@ const mountCiVariableListApp = (containerEl) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers),
+ defaultClient: createDefaultClient(resolvers, cacheConfig),
});
return new Vue({
@@ -81,6 +81,7 @@ const mountLegacyCiVariableListApp = (containerEl) => {
endpoint,
projectId,
isGroup,
+ isProject,
maskableRegex,
protectedByDefault,
awsLogoSvgPath,
@@ -92,6 +93,8 @@ const mountLegacyCiVariableListApp = (containerEl) => {
maskedEnvironmentVariablesLink,
environmentScopeLink,
} = containerEl.dataset;
+
+ const parsedIsProject = parseBoolean(isProject);
const parsedIsGroup = parseBoolean(isGroup);
const isProtectedByDefault = parseBoolean(protectedByDefault);
@@ -99,6 +102,7 @@ const mountLegacyCiVariableListApp = (containerEl) => {
endpoint,
projectId,
isGroup: parsedIsGroup,
+ isProject: parsedIsProject,
maskableRegex,
isProtectedByDefault,
awsLogoSvgPath,
diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js
index 8a182737e7b..ac31e845b0d 100644
--- a/app/assets/javascripts/ci_variable_list/store/actions.js
+++ b/app/assets/javascripts/ci_variable_list/store/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -48,7 +48,7 @@ export const addVariable = ({ state, dispatch }) => {
dispatch('fetchVariables');
})
.catch((error) => {
- createFlash({
+ createAlert({
message: error.response.data[0],
});
dispatch('receiveAddVariableError', error);
@@ -80,7 +80,7 @@ export const updateVariable = ({ state, dispatch }) => {
dispatch('fetchVariables');
})
.catch((error) => {
- createFlash({
+ createAlert({
message: error.response.data[0],
});
dispatch('receiveUpdateVariableError', error);
@@ -109,7 +109,7 @@ export const fetchVariables = ({ dispatch, state }) => {
dispatch('receiveVariablesSuccess', prepareDataForDisplay(data.variables));
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('There was an error fetching the variables.'),
});
});
@@ -139,7 +139,7 @@ export const deleteVariable = ({ dispatch, state }) => {
dispatch('fetchVariables');
})
.catch((error) => {
- createFlash({
+ createAlert({
message: error.response.data[0],
});
dispatch('receiveDeleteVariableError', error);
@@ -162,7 +162,7 @@ export const fetchEnvironments = ({ dispatch, state }) => {
dispatch('receiveEnvironmentsSuccess', prepareEnvironments(res.data));
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('There was an error fetching the environments information.'),
});
});
diff --git a/app/assets/javascripts/ci_variable_list/utils.js b/app/assets/javascripts/ci_variable_list/utils.js
index 1faa97a5f73..eeca69274ce 100644
--- a/app/assets/javascripts/ci_variable_list/utils.js
+++ b/app/assets/javascripts/ci_variable_list/utils.js
@@ -1,3 +1,4 @@
+import * as Sentry from '@sentry/browser';
import { uniq } from 'lodash';
import { allEnvironments } from './constants';
@@ -48,3 +49,12 @@ export const convertEnvironmentScope = (environmentScope = '') => {
export const mapEnvironmentNames = (nodes = []) => {
return nodes.map((env) => env.name);
};
+
+export const reportMessageToSentry = (component, message, context) => {
+ Sentry.withScope((scope) => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ scope.setContext('Vue data', context);
+ scope.setTag('component', component);
+ Sentry.captureMessage(message);
+ });
+};
diff --git a/app/assets/javascripts/clusters/agents/components/activity_events_list.vue b/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
index 18c6503bfb2..ca65665b9ed 100644
--- a/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
+++ b/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
@@ -132,11 +132,7 @@ export default {
:key="key"
class="agent-activity-list issuable-discussion"
>
- <h4
- class="gl-pb-4 gl-ml-5"
- :class="$options.borderClasses"
- data-testid="activity-section-title"
- >
+ <h4 class="gl-pb-4" :class="$options.borderClasses" data-testid="activity-section-title">
{{ key }}
</h4>
diff --git a/app/assets/javascripts/clusters/agents/components/activity_history_item.vue b/app/assets/javascripts/clusters/agents/components/activity_history_item.vue
index 7792d89a575..ed11fe1130d 100644
--- a/app/assets/javascripts/clusters/agents/components/activity_history_item.vue
+++ b/app/assets/javascripts/clusters/agents/components/activity_history_item.vue
@@ -45,8 +45,8 @@ export default {
};
</script>
<template>
- <history-item :icon="eventDetails.eventTypeIcon" class="gl-my-0! gl-pr-0!">
- <strong>
+ <history-item :icon="eventDetails.eventTypeIcon" class="gl-my-0! gl-px-0! gl-pb-0!">
+ <strong class="gl-pl-3 gl-font-lg">
<gl-sprintf :message="eventDetails.title"
><template v-if="eventDetails.titleIcon" #titleIcon
><gl-icon
@@ -61,15 +61,15 @@ export default {
</strong>
<template #body>
- <p class="gl-mt-2 gl-mb-0 gl-pb-2" :class="bodyClass">
+ <p class="gl-mt-2 gl-mb-0 gl-ml-3 gl-pb-3 gl-text-secondary" :class="bodyClass">
<gl-sprintf :message="eventDetails.body">
<template #userName>
- <span class="gl-font-weight-bold">{{ eventDetails.user.name }}</span>
+ <span class="gl-font-weight-bold gl-text-body">{{ eventDetails.user.name }}</span>
<gl-link :href="eventDetails.user.webUrl">@{{ eventDetails.user.username }}</gl-link>
</template>
<template #strong="{ content }">
- <span class="gl-font-weight-bold"> {{ content }} </span>
+ <span class="gl-font-weight-bold gl-text-body"> {{ content }} </span>
</template>
</gl-sprintf>
<time-ago-tooltip :time="eventDetails.recordedAt" />
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index a8fef372637..21524c5b29e 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,7 +1,7 @@
import { GlToast } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import AccessorUtilities from '~/lib/utils/accessor';
import initProjectSelectDropdown from '~/project_select';
import Poll from '~/lib/utils/poll';
@@ -196,7 +196,7 @@ export default class Clusters {
}
static handleError() {
- createFlash({
+ createAlert({
message: s__('ClusterIntegration|Something went wrong on our end.'),
});
}
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index d70b36e63bc..77d6d5eb009 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/browser';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
@@ -70,7 +70,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
commit(types.SET_LOADING_CLUSTERS, false);
commit(types.SET_LOADING_NODES, false);
- createFlash({
+ createAlert({
message: s__('Clusters|An error occurred while loading clusters'),
});
diff --git a/app/assets/javascripts/code_navigation/utils/dom_utils.js b/app/assets/javascripts/code_navigation/utils/dom_utils.js
index 90af31b715c..66770cca8a2 100644
--- a/app/assets/javascripts/code_navigation/utils/dom_utils.js
+++ b/app/assets/javascripts/code_navigation/utils/dom_utils.js
@@ -6,26 +6,28 @@ const isBlank = (str) => !str || /^\s*$/.test(str);
const isMatch = (s1, s2) => !isBlank(s1) && s1.trim() === s2.trim();
-const createSpan = (content) => {
+const createSpan = (content, classList) => {
const span = document.createElement('span');
span.innerText = content;
+ span.classList = classList || '';
return span;
};
-const wrapSpacesWithSpans = (text) => text.replace(/ /g, createSpan(' ').outerHTML);
+const wrapSpacesWithSpans = (text) =>
+ text.replace(/ /g, createSpan(' ').outerHTML).replace(/\t/g, createSpan(' ').outerHTML);
-const wrapTextWithSpan = (el, text) => {
+const wrapTextWithSpan = (el, text, classList) => {
if (isTextNode(el) && isMatch(el.textContent, text)) {
- const newEl = createSpan(text.trim());
+ const newEl = createSpan(text.trim(), classList);
el.replaceWith(newEl);
}
};
-const wrapNodes = (text) => {
+const wrapNodes = (text, classList) => {
const wrapper = createSpan();
// eslint-disable-next-line no-unsanitized/property
wrapper.innerHTML = wrapSpacesWithSpans(text);
- wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text));
+ wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text, classList));
return wrapper.childNodes;
};
diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js
index 46038df2f86..7a5fa9f4a35 100644
--- a/app/assets/javascripts/code_navigation/utils/index.js
+++ b/app/assets/javascripts/code_navigation/utils/index.js
@@ -17,11 +17,9 @@ export const addInteractionClass = ({ path, d, wrapTextNodes }) => {
if (wrapTextNodes) {
line.childNodes.forEach((elm) => {
- if (isTextNode(elm)) {
- // Highlight.js does not wrap all text nodes by default
- // We need all text nodes to be wrapped in order to append code nav attributes
- elm.replaceWith(...wrapNodes(elm.textContent));
- }
+ // Highlight.js does not wrap all text nodes by default
+ // We need all text nodes to be wrapped in order to append code nav attributes
+ elm.replaceWith(...wrapNodes(elm.textContent, elm.classList));
});
}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 6890d7f6f44..b0a1c46e619 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -205,6 +205,12 @@ export default {
mrPipelinesDocsPath: helpPagePath('ci/pipelines/merge_request_pipelines.md', {
anchor: 'prerequisites',
}),
+ runPipelinesInTheParentProjectHelpPath: helpPagePath(
+ '/ci/pipelines/merge_request_pipelines.html',
+ {
+ anchor: 'run-pipelines-in-the-parent-project',
+ },
+ ),
};
</script>
<template>
@@ -321,10 +327,7 @@ export default {
s__('Pipelines|If you are unsure, please ask a project maintainer to review it for you.')
}}
</p>
- <gl-link
- href="/help/ci/pipelines/merge_request_pipelines.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
- target="_blank"
- >
+ <gl-link :href="$options.runPipelinesInTheParentProjectHelpPath" target="_blank">
{{ s__('Pipelines|More Information') }}
</gl-link>
</gl-modal>
diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js
index f973bf51b57..d40cbe589c0 100644
--- a/app/assets/javascripts/commit_merge_requests.js
+++ b/app/assets/javascripts/commit_merge_requests.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import createFlash from './flash';
+import { createAlert } from '~/flash';
import axios from './lib/utils/axios_utils';
import { n__, s__ } from './locale';
@@ -71,7 +71,7 @@ export function fetchCommitMergeRequests() {
$container.html($content);
})
.catch(() =>
- createFlash({
+ createAlert({
message: s__('Commits|An error occurred while fetching merge requests data.'),
}),
);
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index af049738016..e95424eef4d 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Api from '~/api';
import { __ } from '~/locale';
import state from '../state';
@@ -80,7 +80,7 @@ export default {
this.selectProject(this.projects[0]);
})
.catch((e) => {
- createFlash({
+ createAlert({
message: __('Error fetching forked projects. Please try again.'),
});
throw e;
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 659c447e861..22381377389 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -3,7 +3,7 @@ import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { __ } from '~/locale';
import { VARIANT_DANGER } from '~/flash';
import { createContentEditor } from '../services/create_content_editor';
-import { ALERT_EVENT } from '../constants';
+import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants';
import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
@@ -51,6 +51,12 @@ export default {
required: false,
default: '',
},
+ autofocus: {
+ type: [String, Boolean],
+ required: false,
+ default: false,
+ validator: (autofocus) => TIPTAP_AUTOFOCUS_OPTIONS.includes(autofocus),
+ },
},
data() {
return {
@@ -67,7 +73,7 @@ export default {
},
},
created() {
- const { renderMarkdown, uploadsPath, extensions, serializerConfig } = this;
+ const { renderMarkdown, uploadsPath, extensions, serializerConfig, autofocus } = this;
// This is a non-reactive attribute intentionally since this is a complex object.
this.contentEditor = createContentEditor({
@@ -75,6 +81,9 @@ export default {
uploadsPath,
extensions,
serializerConfig,
+ tiptapOptions: {
+ autofocus,
+ },
});
},
mounted() {
@@ -141,7 +150,12 @@ export default {
<template>
<content-editor-provider :content-editor="contentEditor">
<div>
- <editor-state-observer @docUpdate="notifyChange" @focus="focus" @blur="blur" />
+ <editor-state-observer
+ @docUpdate="notifyChange"
+ @focus="focus"
+ @blur="blur"
+ @keydown="$emit('keydown', $event)"
+ />
<content-editor-alert />
<div
data-testid="content-editor"
diff --git a/app/assets/javascripts/content_editor/components/editor_state_observer.vue b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
index 41c3771bf41..ccb46e3b593 100644
--- a/app/assets/javascripts/content_editor/components/editor_state_observer.vue
+++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
@@ -1,6 +1,6 @@
<script>
import { debounce } from 'lodash';
-import { ALERT_EVENT } from '../constants';
+import { ALERT_EVENT, KEYDOWN_EVENT } from '../constants';
export const tiptapToComponentMap = {
update: 'docUpdate',
@@ -10,7 +10,7 @@ export const tiptapToComponentMap = {
blur: 'blur',
};
-export const eventHubEvents = [ALERT_EVENT];
+export const eventHubEvents = [ALERT_EVENT, KEYDOWN_EVENT];
const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName];
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
new file mode 100644
index 00000000000..987b7044272
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -0,0 +1,264 @@
+<script>
+import { GlDropdownItem, GlAvatarLabeled } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdownItem,
+ GlAvatarLabeled,
+ },
+
+ props: {
+ char: {
+ type: String,
+ required: true,
+ },
+
+ nodeType: {
+ type: String,
+ required: true,
+ },
+
+ nodeProps: {
+ type: Object,
+ required: true,
+ },
+
+ items: {
+ type: Array,
+ required: true,
+ },
+
+ command: {
+ type: Function,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ selectedIndex: 0,
+ };
+ },
+
+ computed: {
+ isReference() {
+ return this.nodeType.startsWith('reference');
+ },
+
+ isCommand() {
+ return this.isReference && this.nodeProps.referenceType === 'command';
+ },
+
+ isUser() {
+ return this.isReference && this.nodeProps.referenceType === 'user';
+ },
+
+ isIssue() {
+ return this.isReference && this.nodeProps.referenceType === 'issue';
+ },
+
+ isLabel() {
+ return this.isReference && this.nodeProps.referenceType === 'label';
+ },
+
+ isEpic() {
+ return this.isReference && this.nodeProps.referenceType === 'epic';
+ },
+
+ isSnippet() {
+ return this.isReference && this.nodeProps.referenceType === 'snippet';
+ },
+
+ isVulnerability() {
+ return this.isReference && this.nodeProps.referenceType === 'vulnerability';
+ },
+
+ isMergeRequest() {
+ return this.isReference && this.nodeProps.referenceType === 'merge_request';
+ },
+
+ isMilestone() {
+ return this.isReference && this.nodeProps.referenceType === 'milestone';
+ },
+
+ isEmoji() {
+ return this.nodeType === 'emoji';
+ },
+ },
+
+ watch: {
+ items() {
+ this.selectedIndex = 0;
+ },
+ },
+
+ methods: {
+ getText(item) {
+ if (this.isEmoji) return item.e;
+
+ switch (this.isReference && this.nodeProps.referenceType) {
+ case 'user':
+ return `${this.char}${item.username}`;
+ case 'issue':
+ case 'merge_request':
+ return `${this.char}${item.iid}`;
+ case 'snippet':
+ return `${this.char}${item.id}`;
+ case 'milestone':
+ return `${this.char}${item.title}`;
+ case 'label':
+ return item.title;
+ case 'command':
+ return `${this.char}${item.name}`;
+ case 'epic':
+ return item.reference;
+ case 'vulnerability':
+ return `[vulnerability:${item.id}]`;
+ default:
+ return '';
+ }
+ },
+
+ getProps(item) {
+ const props = {};
+
+ if (this.isEmoji) {
+ Object.assign(props, {
+ name: item.name,
+ unicodeVersion: item.u,
+ title: item.d,
+ moji: item.e,
+ });
+ }
+
+ if (this.isLabel || this.isMilestone) {
+ Object.assign(props, {
+ originalText: `${this.char}${
+ /\W/.test(item.title) ? JSON.stringify(item.title) : item.title
+ }`,
+ });
+ }
+
+ if (this.isLabel) {
+ Object.assign(props, {
+ text: item.title,
+ color: item.color,
+ });
+ }
+
+ Object.assign(props, this.nodeProps);
+
+ return props;
+ },
+
+ onKeyDown({ event }) {
+ if (event.key === 'ArrowUp') {
+ this.upHandler();
+ return true;
+ }
+
+ if (event.key === 'ArrowDown') {
+ this.downHandler();
+ return true;
+ }
+
+ if (event.key === 'Enter') {
+ this.enterHandler();
+ return true;
+ }
+
+ return false;
+ },
+
+ upHandler() {
+ this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length;
+ },
+
+ downHandler() {
+ this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
+ },
+
+ enterHandler() {
+ this.selectItem(this.selectedIndex);
+ },
+
+ selectItem(index) {
+ const item = this.items[index];
+
+ if (item) {
+ this.command({
+ text: this.getText(item),
+ ...this.getProps(item),
+ });
+ }
+ },
+
+ avatarSubLabel(item) {
+ return item.count ? `${item.name} (${item.count})` : item.name;
+ },
+ },
+};
+</script>
+
+<template>
+ <ul
+ :class="{ show: items.length > 0 }"
+ class="gl-new-dropdown dropdown-menu gl-relative"
+ data-testid="content-editor-suggestions-dropdown"
+ >
+ <div class="gl-new-dropdown-inner gl-overflow-y-auto">
+ <gl-dropdown-item
+ v-for="(item, index) in items"
+ :key="index"
+ :class="{ 'gl-bg-gray-50': index === selectedIndex }"
+ @click="selectItem(index)"
+ >
+ <gl-avatar-labeled
+ v-if="isUser"
+ :label="item.username"
+ :sub-label="avatarSubLabel(item)"
+ :src="item.avatar_url"
+ :entity-name="item.username"
+ :shape="item.type === 'Group' ? 'rect' : 'circle'"
+ :size="32"
+ />
+ <span v-if="isIssue || isMergeRequest">
+ <small>{{ item.iid }}</small>
+ {{ item.title }}
+ </span>
+ <span v-if="isVulnerability || isSnippet">
+ <small>{{ item.id }}</small>
+ {{ item.title }}
+ </span>
+ <span v-if="isEpic">
+ <small>{{ item.reference }}</small>
+ {{ item.title }}
+ </span>
+ <span v-if="isMilestone">
+ {{ item.title }}
+ </span>
+ <span v-if="isLabel" class="gl-display-flex gl-align-items-center">
+ <span
+ data-testid="label-color-box"
+ class="gl-rounded-base gl-display-block gl-w-5 gl-h-5 gl-mr-3"
+ :style="{ backgroundColor: item.color }"
+ ></span>
+ {{ item.title }}
+ </span>
+ <span v-if="isCommand">
+ /{{ item.name }} <small> {{ item.params[0] }} </small><br />
+ <em>
+ <small> {{ item.description }} </small>
+ </em>
+ </span>
+ <div v-if="isEmoji" class="gl-display-flex gl-align-items-center">
+ <div class="gl-pr-4 gl-font-lg">{{ item.e }}</div>
+ <div class="gl-flex-grow-1">
+ {{ item.name }}<br />
+ <small>{{ item.d }}</small>
+ </div>
+ </div>
+ </gl-dropdown-item>
+ </div>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/label.vue b/app/assets/javascripts/content_editor/components/wrappers/label.vue
new file mode 100644
index 00000000000..4206c866032
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/label.vue
@@ -0,0 +1,34 @@
+<script>
+import { NodeViewWrapper } from '@tiptap/vue-2';
+import { GlLabel } from '@gitlab/ui';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+
+export default {
+ name: 'DetailsWrapper',
+ components: {
+ NodeViewWrapper,
+ GlLabel,
+ },
+ props: {
+ node: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ isScopedLabel() {
+ return isScopedLabel({ title: this.node.attrs.originalText });
+ },
+ },
+};
+</script>
+<template>
+ <node-view-wrapper class="gl-display-inline-block">
+ <gl-label
+ size="sm"
+ :scoped="isScopedLabel"
+ :background-color="node.attrs.color"
+ :title="node.attrs.text"
+ />
+ </node-view-wrapper>
+</template>
diff --git a/app/assets/javascripts/content_editor/constants/index.js b/app/assets/javascripts/content_editor/constants/index.js
index 564cca23afa..14862727811 100644
--- a/app/assets/javascripts/content_editor/constants/index.js
+++ b/app/assets/javascripts/content_editor/constants/index.js
@@ -42,10 +42,8 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [
},
];
-export const LOADING_CONTENT_EVENT = 'loading';
-export const LOADING_SUCCESS_EVENT = 'loadingSuccess';
-export const LOADING_ERROR_EVENT = 'loadingError';
export const ALERT_EVENT = 'alert';
+export const KEYDOWN_EVENT = 'keydown';
export const PARSE_HTML_PRIORITY_LOWEST = 1;
export const PARSE_HTML_PRIORITY_DEFAULT = 50;
@@ -66,3 +64,5 @@ export const SAFE_VIDEO_EXT = ['mp4', 'm4v', 'mov', 'webm', 'ogv'];
export const SAFE_AUDIO_EXT = ['mp3', 'oga', 'ogg', 'spx', 'wav'];
export const DIAGRAM_LANGUAGES = ['plantuml', 'mermaid'];
+
+export const TIPTAP_AUTOFOCUS_OPTIONS = [true, false, 'start', 'end', 'all'];
diff --git a/app/assets/javascripts/content_editor/extensions/diagram.js b/app/assets/javascripts/content_editor/extensions/diagram.js
index d9983b8c1c5..7c4a56468eb 100644
--- a/app/assets/javascripts/content_editor/extensions/diagram.js
+++ b/app/assets/javascripts/content_editor/extensions/diagram.js
@@ -1,5 +1,6 @@
import { lowlight } from 'lowlight/lib/core';
import { textblockTypeInputRule } from '@tiptap/core';
+import { base64DecodeUnicode } from '~/lib/utils/text_utility';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
import languageLoader from '../services/code_block_language_loader';
import CodeBlockHighlight from './code_block_highlight';
@@ -45,7 +46,9 @@ export default CodeBlockHighlight.extend({
priority: PARSE_HTML_PRIORITY_HIGHEST,
tag: '[data-diagram]',
getContent(element, schema) {
- const source = atob(element.dataset.diagramSrc.replace('data:text/plain;base64,', ''));
+ const source = base64DecodeUnicode(
+ element.dataset.diagramSrc.replace('data:text/plain;base64,', ''),
+ );
const node = schema.node('paragraph', {}, [schema.text(source)]);
return node.content;
},
diff --git a/app/assets/javascripts/content_editor/extensions/external_keydown_handler.js b/app/assets/javascripts/content_editor/extensions/external_keydown_handler.js
new file mode 100644
index 00000000000..e940614083e
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/external_keydown_handler.js
@@ -0,0 +1,38 @@
+import { Extension } from '@tiptap/core';
+import { Plugin, PluginKey } from 'prosemirror-state';
+import { KEYDOWN_EVENT } from '../constants';
+
+/**
+ * This extension bubbles up the keydown event, captured by ProseMirror in the
+ * contenteditale element, to the presentation layer implemented in vue.
+ *
+ * The purpose of this mechanism is allowing clients of the
+ * content editor to attach keyboard shortcuts for behavior outside
+ * of the Content Editor’s boundaries, i.e. submitting a form to save changes.
+ */
+export default Extension.create({
+ name: 'keyboardShortcut',
+ addOptions() {
+ return {
+ eventHub: null,
+ };
+ },
+ addProseMirrorPlugins() {
+ return [
+ new Plugin({
+ key: new PluginKey('keyboardShortcut'),
+ props: {
+ handleKeyDown: (_, event) => {
+ const {
+ options: { eventHub },
+ } = this;
+
+ eventHub.$emit(KEYDOWN_EVENT, event);
+
+ return false;
+ },
+ },
+ }),
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/heading.js b/app/assets/javascripts/content_editor/extensions/heading.js
index 48303cdeca4..41903162ba5 100644
--- a/app/assets/javascripts/content_editor/extensions/heading.js
+++ b/app/assets/javascripts/content_editor/extensions/heading.js
@@ -1 +1,15 @@
-export { Heading as default } from '@tiptap/extension-heading';
+import { Heading } from '@tiptap/extension-heading';
+import { textblockTypeInputRule } from '@tiptap/core';
+
+export default Heading.extend({
+ addInputRules() {
+ return this.options.levels.map((level) => {
+ return textblockTypeInputRule({
+ // make sure heading regex doesn't conflict with issue references
+ find: new RegExp(`^(#{1,${level}})[ \t]$`),
+ type: this.type,
+ getAttributes: { level },
+ });
+ });
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js
index 5e459e65de2..707beaf1231 100644
--- a/app/assets/javascripts/content_editor/extensions/reference.js
+++ b/app/assets/javascripts/content_editor/extensions/reference.js
@@ -46,22 +46,10 @@ export default Node.create({
tag: 'a.gfm:not([data-link=true])',
priority: PARSE_HTML_PRIORITY_HIGHEST,
},
- {
- tag: 'span.gl-label',
- },
];
},
renderHTML({ node }) {
- return [
- 'a',
- {
- class: node.attrs.className,
- href: node.attrs.href,
- 'data-reference-type': node.attrs.referenceType,
- 'data-original': node.attrs.originalText,
- },
- node.attrs.text,
- ];
+ return ['a', { href: '#' }, node.attrs.text];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/reference_label.js b/app/assets/javascripts/content_editor/extensions/reference_label.js
new file mode 100644
index 00000000000..716e191c3d5
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/reference_label.js
@@ -0,0 +1,35 @@
+import { VueNodeViewRenderer } from '@tiptap/vue-2';
+import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import LabelWrapper from '../components/wrappers/label.vue';
+import Reference from './reference';
+
+export default Reference.extend({
+ name: 'reference_label',
+
+ addAttributes() {
+ return {
+ ...this.parent(),
+ text: {
+ default: null,
+ parseHTML: (element) => {
+ const text = element.querySelector('.gl-label-text').textContent;
+ const scopedText = element.querySelector('.gl-label-text-scoped')?.textContent;
+ if (!scopedText) return text;
+ return `${text}${SCOPED_LABEL_DELIMITER}${scopedText}`;
+ },
+ },
+ color: {
+ default: null,
+ parseHTML: (element) => element.querySelector('.gl-label-text').style.backgroundColor,
+ },
+ };
+ },
+
+ parseHTML() {
+ return [{ tag: 'span.gl-label' }];
+ },
+
+ addNodeView() {
+ return new VueNodeViewRenderer(LabelWrapper);
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js
new file mode 100644
index 00000000000..8976b9cafee
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/suggestions.js
@@ -0,0 +1,227 @@
+import { Node } from '@tiptap/core';
+import { VueRenderer } from '@tiptap/vue-2';
+import tippy from 'tippy.js';
+import Suggestion from '@tiptap/suggestion';
+import { PluginKey } from 'prosemirror-state';
+import { isFunction, uniqueId, memoize } from 'lodash';
+import axios from '~/lib/utils/axios_utils';
+import { initEmojiMap, getAllEmoji } from '~/emoji';
+import SuggestionsDropdown from '../components/suggestions_dropdown.vue';
+
+function find(haystack, needle) {
+ return String(haystack).toLocaleLowerCase().includes(String(needle).toLocaleLowerCase());
+}
+
+function createSuggestionPlugin({
+ editor,
+ char,
+ dataSource,
+ search,
+ limit = Infinity,
+ nodeType,
+ nodeProps = {},
+}) {
+ const fetchData = memoize(
+ isFunction(dataSource) ? dataSource : async () => (await axios.get(dataSource)).data,
+ );
+
+ return Suggestion({
+ editor,
+ char,
+ pluginKey: new PluginKey(uniqueId('suggestions')),
+
+ command: ({ editor: tiptapEditor, range, props }) => {
+ tiptapEditor
+ .chain()
+ .focus()
+ .insertContentAt(range, [
+ { type: nodeType, attrs: props },
+ { type: 'text', text: ' ' },
+ ])
+ .run();
+ },
+
+ async items({ query }) {
+ if (!dataSource) return [];
+
+ try {
+ const items = await fetchData();
+
+ return items.filter(search(query)).slice(0, limit);
+ } catch {
+ return [];
+ }
+ },
+
+ render: () => {
+ let component;
+ let popup;
+
+ return {
+ onStart: (props) => {
+ component = new VueRenderer(SuggestionsDropdown, {
+ propsData: {
+ ...props,
+ char,
+ nodeType,
+ nodeProps,
+ },
+ editor: props.editor,
+ });
+
+ if (!props.clientRect) {
+ return;
+ }
+
+ popup = tippy('body', {
+ getReferenceClientRect: props.clientRect,
+ appendTo: () => document.body,
+ content: component.element,
+ showOnCreate: true,
+ interactive: true,
+ trigger: 'manual',
+ placement: 'bottom-start',
+ });
+ },
+
+ onUpdate(props) {
+ component?.updateProps(props);
+
+ if (!props.clientRect) {
+ return;
+ }
+
+ popup?.[0].setProps({
+ getReferenceClientRect: props.clientRect,
+ });
+ },
+
+ onKeyDown(props) {
+ if (props.event.key === 'Escape') {
+ popup?.[0].hide();
+
+ return true;
+ }
+
+ return component?.ref?.onKeyDown(props);
+ },
+
+ onExit() {
+ popup?.[0].destroy();
+ component?.destroy();
+ },
+ };
+ },
+ });
+}
+
+export default Node.create({
+ name: 'suggestions',
+
+ addProseMirrorPlugins() {
+ return [
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '@',
+ dataSource: gl.GfmAutoComplete?.dataSources.members,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'user',
+ },
+ search: (query) => ({ name, username }) => find(name, query) || find(username, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '#',
+ dataSource: gl.GfmAutoComplete?.dataSources.issues,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'issue',
+ },
+ search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '$',
+ dataSource: gl.GfmAutoComplete?.dataSources.snippets,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'snippet',
+ },
+ search: (query) => ({ id, title }) => find(id, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '~',
+ dataSource: gl.GfmAutoComplete?.dataSources.labels,
+ nodeType: 'reference_label',
+ nodeProps: {
+ referenceType: 'label',
+ },
+ search: (query) => ({ title }) => find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '&',
+ dataSource: gl.GfmAutoComplete?.dataSources.epics,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'epic',
+ },
+ search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '[vulnerability:',
+ dataSource: gl.GfmAutoComplete?.dataSources.vulnerabilities,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'vulnerability',
+ },
+ search: (query) => ({ id, title }) => find(id, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '!',
+ dataSource: gl.GfmAutoComplete?.dataSources.mergeRequests,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'merge_request',
+ },
+ search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '%',
+ dataSource: gl.GfmAutoComplete?.dataSources.milestones,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'milestone',
+ },
+ search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '/',
+ dataSource: gl.GfmAutoComplete?.dataSources.commands,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'command',
+ },
+ search: (query) => ({ name }) => find(name, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: ':',
+ dataSource: () => Object.values(getAllEmoji()),
+ nodeType: 'emoji',
+ search: (query) => ({ d, name }) => find(d, query) || find(name, query),
+ limit: 10,
+ }),
+ ];
+ },
+
+ onCreate() {
+ initEmojiMap();
+ },
+});
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 5ed7f3dc23d..0d78390e769 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -18,6 +18,7 @@ import Diagram from '../extensions/diagram';
import Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor';
import Emoji from '../extensions/emoji';
+import ExternalKeydownHandler from '../extensions/external_keydown_handler';
import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption';
import FootnoteDefinition from '../extensions/footnote_definition';
@@ -42,10 +43,12 @@ import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
import PasteMarkdown from '../extensions/paste_markdown';
import Reference from '../extensions/reference';
+import ReferenceLabel from '../extensions/reference_label';
import ReferenceDefinition from '../extensions/reference_definition';
import Sourcemap from '../extensions/sourcemap';
import Strike from '../extensions/strike';
import Subscript from '../extensions/subscript';
+import Suggestions from '../extensions/suggestions';
import Superscript from '../extensions/superscript';
import Table from '../extensions/table';
import TableCell from '../extensions/table_cell';
@@ -121,6 +124,7 @@ export const createContentEditor = ({
Image,
InlineDiff,
Italic,
+ ExternalKeydownHandler.configure({ eventHub }),
Link,
ListItem,
Loading,
@@ -129,10 +133,12 @@ export const createContentEditor = ({
Paragraph,
PasteMarkdown.configure({ eventHub, renderMarkdown }),
Reference,
+ ReferenceLabel,
ReferenceDefinition,
Sourcemap,
Strike,
Subscript,
+ Suggestions,
Superscript,
TableCell,
TableHeader,
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index ba0cad6c91c..c990f6cf0b3 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -33,6 +33,7 @@ import MathInline from '../extensions/math_inline';
import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
import Reference from '../extensions/reference';
+import ReferenceLabel from '../extensions/reference_label';
import ReferenceDefinition from '../extensions/reference_definition';
import Strike from '../extensions/strike';
import Subscript from '../extensions/subscript';
@@ -61,6 +62,7 @@ import {
renderHTMLNode,
renderContent,
renderBulletList,
+ renderReference,
preserveUnchanged,
bold,
italic,
@@ -184,9 +186,8 @@ const defaultSerializerConfig = {
[ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item),
[OrderedList.name]: preserveUnchanged(renderOrderedList),
[Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph),
- [Reference.name]: (state, node) => {
- state.write(node.attrs.originalText || node.attrs.text);
- },
+ [Reference.name]: renderReference,
+ [ReferenceLabel.name]: renderReference,
[ReferenceDefinition.name]: preserveUnchanged({
render: (state, node, parent, index, same, sourceMarkdown) => {
const nextSibling = parent.maybeChild(index + 1);
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 41114571df7..5c0cb21075a 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -280,6 +280,7 @@ export function renderTableRow(state, node) {
}
export function renderTable(state, node) {
+ state.flushClose();
setIsInBlockTable(node, shouldRenderHTMLTable(node));
if (isInBlockTable(node)) renderTagOpen(state, 'table');
@@ -422,6 +423,10 @@ export function renderOrderedList(state, node) {
});
}
+export function renderReference(state, node) {
+ state.write(node.attrs.originalText || node.attrs.text);
+}
+
const generateBoldTags = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1];
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
index 512f060e2ea..4e4c21328ca 100644
--- a/app/assets/javascripts/contributors/components/contributors.vue
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -196,7 +196,7 @@ export default {
<template>
<div>
- <div v-if="loading" class="contributors-loader text-center">
+ <div v-if="loading" class="gl-text-center gl-pt-13">
<gl-loading-icon :inline="true" size="xl" />
</div>
diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js
index 4cc0a6a6509..3a6f4191031 100644
--- a/app/assets/javascripts/contributors/stores/actions.js
+++ b/app/assets/javascripts/contributors/stores/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import service from '../services/contributors_service';
import * as types from './mutation_types';
@@ -14,7 +14,7 @@ export const fetchChartData = ({ commit }, endpoint) => {
commit(types.SET_LOADING_STATE, false);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while loading chart data'),
}),
);
diff --git a/app/assets/javascripts/crm/contacts/components/contacts_root.vue b/app/assets/javascripts/crm/contacts/components/contacts_root.vue
index 562363ff88e..2be17d1f80f 100644
--- a/app/assets/javascripts/crm/contacts/components/contacts_root.vue
+++ b/app/assets/javascripts/crm/contacts/components/contacts_root.vue
@@ -86,7 +86,7 @@ export default {
},
methods: {
errorAlertDismissed() {
- this.error = true;
+ this.error = false;
},
extractContacts(data) {
const contacts = data?.group?.contacts?.nodes || [];
@@ -146,7 +146,7 @@ export default {
editButtonLabel: __('Edit'),
title: s__('Crm|Customer relations contacts'),
newContact: s__('Crm|New contact'),
- errorText: __('Something went wrong. Please try again.'),
+ errorMsg: __('Something went wrong. Please try again.'),
},
EDIT_ROUTE_NAME,
NEW_ROUTE_NAME,
@@ -176,7 +176,7 @@ export default {
<div>
<paginated-table-with-search-and-tabs
:show-items="true"
- :show-error-msg="false"
+ :show-error-msg="error"
:i18n="$options.i18n"
:items="contacts.list"
:page-info="contacts.pageInfo"
@@ -243,10 +243,7 @@ export default {
</template>
<template #empty>
- <span v-if="error">
- {{ $options.i18n.errorText }}
- </span>
- <span v-else>
+ <span>
{{ $options.i18n.emptyText }}
</span>
</template>
diff --git a/app/assets/javascripts/crm/organizations/components/organizations_root.vue b/app/assets/javascripts/crm/organizations/components/organizations_root.vue
index 155c8f00537..28f0b34f031 100644
--- a/app/assets/javascripts/crm/organizations/components/organizations_root.vue
+++ b/app/assets/javascripts/crm/organizations/components/organizations_root.vue
@@ -137,7 +137,7 @@ export default {
editButtonLabel: __('Edit'),
title: s__('Crm|Customer relations organizations'),
newOrganization: s__('Crm|New organization'),
- errorText: __('Something went wrong. Please try again.'),
+ errorMsg: __('Something went wrong. Please try again.'),
},
EDIT_ROUTE_NAME,
NEW_ROUTE_NAME,
@@ -167,7 +167,7 @@ export default {
<div>
<paginated-table-with-search-and-tabs
:show-items="true"
- :show-error-msg="false"
+ :show-error-msg="error"
:i18n="$options.i18n"
:items="organizations.list"
:page-info="organizations.pageInfo"
@@ -238,10 +238,7 @@ export default {
</template>
<template #empty>
- <span v-if="error">
- {{ $options.i18n.errorText }}
- </span>
- <span v-else>
+ <span>
{{ $options.i18n.emptyText }}
</span>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index 5c2e29bfa74..4a201e00582 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -6,7 +6,7 @@ import {
getValueStreamStageCounts,
} from '~/api/analytics_api';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { DEFAULT_VALUE_STREAM, I18N_VSA_ERROR_STAGE_MEDIAN } from '../constants';
import * as types from './mutation_types';
@@ -97,7 +97,7 @@ export const fetchStageMedians = ({
.then((data) => commit(types.RECEIVE_STAGE_MEDIANS_SUCCESS, data))
.catch((error) => {
commit(types.RECEIVE_STAGE_MEDIANS_ERROR, error);
- createFlash({ message: I18N_VSA_ERROR_STAGE_MEDIAN });
+ createAlert({ message: I18N_VSA_ERROR_STAGE_MEDIAN });
});
};
@@ -126,7 +126,7 @@ export const fetchStageCountValues = ({
.then((data) => commit(types.RECEIVE_STAGE_COUNTS_SUCCESS, data))
.catch((error) => {
commit(types.RECEIVE_STAGE_COUNTS_ERROR, error);
- createFlash({
+ createAlert({
message: __('There was an error fetching stage total counts'),
});
});
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
index 814a4b672a2..c67b544eacd 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
@@ -3,7 +3,7 @@ import { GlFormGroup, GlFormInput, GlModal, GlSprintf, GlLink } from '@gitlab/ui
import { isValidCron } from 'cron-validator';
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
-import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import { mapComputed } from '~/vuex_shared/bindings';
export default {
diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js
index 1ac6781a0e3..76a4eaaff3f 100644
--- a/app/assets/javascripts/deploy_freeze/store/actions.js
+++ b/app/assets/javascripts/deploy_freeze/store/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { logError } from '~/lib/logger';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -27,7 +27,7 @@ const receiveFreezePeriod = (store, request) => {
dispatch('fetchFreezePeriods');
})
.catch((error) => {
- createFlash({
+ createAlert({
message: __('Error: Unable to create deploy freeze'),
});
dispatch('receiveFreezePeriodError', error);
@@ -59,7 +59,7 @@ export const deleteFreezePeriod = ({ state, commit }, { id }) => {
return Api.deleteFreezePeriod(state.projectId, id)
.then(() => commit(types.RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS, id))
.catch((e) => {
- createFlash({
+ createAlert({
message: __('Error: Unable to delete deploy freeze'),
});
commit(types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR, id);
@@ -76,7 +76,7 @@ export const fetchFreezePeriods = ({ commit, state }) => {
commit(types.RECEIVE_FREEZE_PERIODS_SUCCESS, data);
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('There was an error fetching the deploy freezes.'),
});
});
diff --git a/app/assets/javascripts/deploy_freeze/store/mutations.js b/app/assets/javascripts/deploy_freeze/store/mutations.js
index 151f7f39f5a..edb455a8dd5 100644
--- a/app/assets/javascripts/deploy_freeze/store/mutations.js
+++ b/app/assets/javascripts/deploy_freeze/store/mutations.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { secondsToHours } from '~/lib/utils/datetime_utility';
+import { formatTimezone } from '~/lib/utils/datetime_utility';
import * as types from './mutation_types';
const formatTimezoneName = (freezePeriod, timezoneList) => {
@@ -8,7 +8,7 @@ const formatTimezoneName = (freezePeriod, timezoneList) => {
return convertObjectPropsToCamelCase({
...freezePeriod,
cron_timezone: {
- formattedTimezone: tz && `[UTC ${secondsToHours(tz.offset)}] ${tz.name}`,
+ formattedTimezone: tz && formatTimezone(tz),
identifier: freezePeriod.cron_timezone,
},
});
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 36d54f586f1..db5e9a954cf 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
@@ -93,7 +93,7 @@ export default {
.catch(() => {
this.isLoading = false;
this.store.keys = {};
- return createFlash({
+ return createAlert({
message: s__('DeployKeys|Error getting deploy keys'),
});
});
@@ -103,7 +103,7 @@ export default {
.enableKey(deployKey.id)
.then(this.fetchKeys)
.catch(() =>
- createFlash({
+ createAlert({
message: s__('DeployKeys|Error enabling deploy key'),
}),
);
@@ -119,7 +119,7 @@ export default {
.then(this.fetchKeys)
.then(hideModal)
.catch(() =>
- createFlash({
+ createAlert({
message: s__('DeployKeys|Error removing deploy key'),
}),
);
diff --git a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
new file mode 100644
index 00000000000..639dd21bd7b
--- /dev/null
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -0,0 +1,332 @@
+<script>
+import {
+ GlFormGroup,
+ GlFormInput,
+ GlFormCheckbox,
+ GlButton,
+ GlDatepicker,
+ GlFormInputGroup,
+ GlSprintf,
+ GlLink,
+} from '@gitlab/ui';
+import { createAlert, VARIANT_INFO } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { s__ } from '~/locale';
+
+function defaultData() {
+ return {
+ expiresAt: null,
+ name: '',
+ newTokenDetails: null,
+ readRepository: false,
+ writeRepository: false,
+ readRegistry: false,
+ writeRegistry: false,
+ readPackageRegistry: false,
+ writePackageRegistry: false,
+ username: '',
+ placeholders: {
+ link: { link: ['link_start', 'link_end'] },
+ i: { i: ['i_start', 'i_end'] },
+ code: { code: ['code_start', 'code_end'] },
+ },
+ };
+}
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ GlDatepicker,
+ GlFormCheckbox,
+ GlButton,
+ GlFormInputGroup,
+ ClipboardButton,
+ GlSprintf,
+ GlLink,
+ },
+
+ props: {
+ createNewTokenPath: {
+ type: String,
+ required: true,
+ },
+ deployTokensHelpUrl: {
+ type: String,
+ required: true,
+ },
+ containerRegistryEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ packagesRegistryEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ tokenType: {
+ type: String,
+ required: true,
+ },
+ },
+
+ data() {
+ return defaultData();
+ },
+ translations: {
+ addTokenButton: s__('DeployTokens|Create deploy token'),
+ addTokenExpiryLabel: s__('DeployTokens|Expiration date (optional)'),
+ addTokenExpiryDescription: s__(
+ 'DeployTokens|Enter an expiration date for your token. Defaults to never expire.',
+ ),
+ addTokenHeader: s__('DeployTokens|New deploy token'),
+ addTokenDescription: s__(
+ 'DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}',
+ ),
+ addTokenNameLabel: s__('DeployTokens|Name'),
+ addTokenNameDescription: s__('DeployTokens|Enter a unique name for your deploy token.'),
+ addTokenScopesLabel: s__('DeployTokens|Scopes (select at least one)'),
+ addTokenUsernameDescription: s__(
+ 'DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}.',
+ ),
+ addTokenUsernameLabel: s__('DeployTokens|Username (optional)'),
+ newTokenCopyMessage: s__('DeployTokens|Copy deploy token'),
+ newProjectTokenCreated: s__('DeployTokens|Your new project deploy token has been created.'),
+ newGroupTokenCreated: s__('DeployTokens|Your new group deploy token has been created.'),
+ newTokenDescription: s__(
+ 'DeployTokens|Use this token as a password. Save it. This password can %{i_start}not%{i_end} be recovered.',
+ ),
+ newTokenMessage: s__('DeployTokens|Your New Deploy Token'),
+ newTokenUsernameCopy: s__('DeployTokens|Copy username'),
+ newTokenUsernameDescription: s__(
+ 'DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}',
+ ),
+ readRepositoryHelp: s__('DeployTokens|Allows read-only access to the repository.'),
+ readRegistryHelp: s__('DeployTokens|Allows read-only access to registry images.'),
+ writeRegistryHelp: s__('DeployTokens|Allows read and write access to registry images.'),
+ readPackageRegistryHelp: s__('DeployTokens|Allows read-only access to the package registry.'),
+ writePackageRegistryHelp: s__(
+ 'DeployTokens|Allows read and write access to the package registry.',
+ ),
+ },
+ computed: {
+ formattedExpiryDate() {
+ return formatDate(this.expiresAt, 'yyyy-mm-dd');
+ },
+ newTokenCreatedMessage() {
+ return this.tokenType === 'group'
+ ? this.$options.translations.newGroupTokenCreated
+ : this.$options.translations.newProjectTokenCreated;
+ },
+ },
+ methods: {
+ createDeployToken() {
+ return axios
+ .post(this.createNewTokenPath, {
+ deploy_token: {
+ expires_at: this.expiresAt,
+ name: this.name,
+ read_repository: this.readRepository,
+ read_registry: this.readRegistry,
+ username: this.username,
+ },
+ })
+ .then((response) => {
+ this.newTokenDetails = response.data;
+ this.resetData();
+ createAlert({
+ variant: VARIANT_INFO,
+ message: this.newTokenCreatedMessage,
+ });
+ })
+ .catch((error) => {
+ createAlert({
+ message: error.response.data.message,
+ });
+ });
+ },
+ resetData() {
+ const newData = defaultData();
+ delete newData.newTokenDetails;
+ Object.keys(newData).forEach((k) => {
+ this[k] = newData[k];
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div v-if="newTokenDetails" class="created-deploy-token-container info-well">
+ <div class="well-segment">
+ <h5>{{ $options.translations.newTokenMessage }}</h5>
+ <gl-form-group>
+ <template #description>
+ <div class="deploy-token-help-block gl-mt-2 text-success">
+ <gl-sprintf
+ :message="$options.translations.newTokenUsernameDescription"
+ :placeholders="placeholders.link"
+ >
+ <template #link="{ content }">
+ <gl-link :href="deployTokensHelpUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </template>
+ <gl-form-input-group
+ name="deploy-token-user"
+ :value="newTokenDetails.username"
+ select-on-click
+ readonly
+ >
+ <template #append>
+ <clipboard-button
+ :text="newTokenDetails.username"
+ :title="$options.translations.newTokenUsernameCopy"
+ />
+ </template>
+ </gl-form-input-group>
+ </gl-form-group>
+ <gl-form-group>
+ <template #description>
+ <div class="deploy-token-help-block gl-mt-2 text-danger">
+ <gl-sprintf
+ :message="$options.translations.newTokenDescription"
+ :placeholders="placeholders.i"
+ >
+ <template #i="{ content }">
+ <i>{{ content }}</i>
+ </template>
+ </gl-sprintf>
+ </div>
+ </template>
+ <gl-form-input-group :value="newTokenDetails.token" name="deploy-token" readonly>
+ <template #append>
+ <clipboard-button
+ :text="newTokenDetails.token"
+ :title="$options.translations.newTokenCopyMessage"
+ />
+ </template>
+ </gl-form-input-group>
+ </gl-form-group>
+ </div>
+ </div>
+ <h5>{{ $options.translations.addTokenHeader }}</h5>
+ <p class="profile-settings-content">
+ <gl-sprintf
+ :message="$options.translations.addTokenDescription"
+ :placeholders="placeholders.link"
+ >
+ <template #link="{ content }">
+ <gl-link :href="deployTokensHelpUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-form-group
+ :label="$options.translations.addTokenNameLabel"
+ :description="$options.translations.addTokenNameDescription"
+ label-for="deploy_token_name"
+ >
+ <gl-form-input
+ id="deploy_token_name"
+ v-model="name"
+ name="deploy_token_name"
+ class="qa-deploy-token-name"
+ data-qa-selector="deploy_token_name_field"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.translations.addTokenExpiryLabel"
+ :description="$options.translations.addTokenExpiryDescription"
+ label-for="deploy_token_expires_at"
+ >
+ <gl-form-input
+ id="deploy_token_expires_at"
+ name="deploy_token_expires_at"
+ :value="formattedExpiryDate"
+ data-qa-selector="deploy_token_expires_at_field"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.translations.addTokenUsernameLabel"
+ label-for="deploy_token_username"
+ >
+ <template #description>
+ <gl-sprintf
+ :message="$options.translations.addTokenUsernameDescription"
+ :placeholders="placeholders.code"
+ >
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </template>
+ <gl-form-input id="deploy_token_username" v-model="username" />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.translations.addTokenScopesLabel"
+ label-for="deploy-token-scopes"
+ >
+ <div id="deploy-token-scopes">
+ <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
+ <gl-form-checkbox
+ id="deploy_token_read_repository"
+ v-model="readRepository"
+ name="deploy_token_read_repository"
+ data-qa-selector="deploy_token_read_repository_checkbox"
+ >
+ read_repository
+ <template #help>{{ $options.translations.readRepositoryHelp }}</template>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ v-if="containerRegistryEnabled"
+ id="deploy_token_read_registry"
+ v-model="readRegistry"
+ name="deploy_token_read_registry"
+ data-qa-selector="deploy_token_read_registry_checkbox"
+ >
+ read_registry
+ <template #help>{{ $options.translations.readRegistryHelp }}</template>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ v-if="containerRegistryEnabled"
+ id="deploy_token_write_registry"
+ v-model="writeRegistry"
+ name="deploy_token_write_registry"
+ data-qa-selector="deploy_token_write_registry_checkbox"
+ >
+ write_registry
+ <template #help>{{ $options.translations.writeRegistryHelp }}</template>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ v-if="packagesRegistryEnabled"
+ id="deploy_token_read_package_registry"
+ v-model="readPackageRegistry"
+ name="deploy_token_read_package_registry"
+ data-qa-selector="deploy_token_read_package_registry_checkbox"
+ >
+ read_package_registry
+ <template #help>{{ $options.translations.readPackageRegistryHelp }}</template>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ v-if="packagesRegistryEnabled"
+ id="deploy_token_write_package_registry"
+ v-model="writePackageRegistry"
+ name="deploy_token_write_package_registry"
+ data-qa-selector="deploy_token_write_package_registry_checkbox"
+ >
+ write_package_registry
+ <template #help>{{ $options.translations.writePackageRegistryHelp }}</template>
+ </gl-form-checkbox>
+ <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
+ </div>
+ </gl-form-group>
+ <div>
+ <gl-button variant="success" @click="createDeployToken">
+ {{ $options.translations.addTokenButton }}
+ </gl-button>
+ </div>
+ <gl-datepicker v-model="expiresAt" target="#deploy_token_expires_at" container="body" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/deploy_tokens/index.js b/app/assets/javascripts/deploy_tokens/index.js
new file mode 100644
index 00000000000..334c9930f4b
--- /dev/null
+++ b/app/assets/javascripts/deploy_tokens/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import NewDeployToken from './components/new_deploy_token.vue';
+
+export default function initDeployTokens() {
+ const el = document.getElementById('js-new-deploy-token');
+
+ if (el == null) return null;
+
+ const {
+ createNewTokenPath,
+ deployTokensHelpUrl,
+ containerRegistryEnabled,
+ packagesRegistryEnabled,
+ tokenType,
+ } = el.dataset;
+ return new Vue({
+ el,
+ components: {
+ NewDeployToken,
+ },
+ render(createElement) {
+ return createElement(NewDeployToken, {
+ props: {
+ createNewTokenPath,
+ deployTokensHelpUrl,
+ containerRegistryEnabled: containerRegistryEnabled !== undefined,
+ packagesRegistryEnabled: packagesRegistryEnabled !== undefined,
+ tokenType,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index 124780df8a5..a4430b15752 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
@@ -155,7 +155,7 @@ export default {
methods: {
onDone({ data: { createNote } }) {
if (hasErrors(createNote)) {
- createFlash({ message: ADD_DISCUSSION_COMMENT_ERROR });
+ createAlert({ message: ADD_DISCUSSION_COMMENT_ERROR });
}
this.discussionComment = '';
this.hideForm();
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index 4faeba3983b..5a6b220e532 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlModal } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import $ from 'jquery';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
@@ -7,13 +7,23 @@ import Autosave from '~/autosave';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
export default {
name: 'DesignReplyForm',
+ i18n: {
+ primaryBtn: s__('DesignManagement|Discard changes'),
+ cancelBtnCreate: s__('DesignManagement|Continue creating'),
+ cancelBtnUpdate: s__('DesignManagement|Continue editing'),
+ cancelCreate: s__('DesignManagement|Are you sure you want to cancel creating this comment?'),
+ cancelUpdate: s__('DesignManagement|Are you sure you want to cancel editing this comment?'),
+ newCommentButton: s__('DesignManagement|Comment'),
+ updateCommentButton: s__('DesignManagement|Save comment'),
+ },
+ markdownDocsPath: helpPagePath('user/markdown'),
components: {
MarkdownField,
GlButton,
- GlModal,
},
props: {
markdownPreviewPath: {
@@ -54,29 +64,10 @@ export default {
hasValue() {
return this.value.trim().length > 0;
},
- modalSettings() {
- if (this.isNewComment) {
- return {
- title: s__('DesignManagement|Cancel comment confirmation'),
- okTitle: s__('DesignManagement|Discard comment'),
- cancelTitle: s__('DesignManagement|Keep comment'),
- content: s__('DesignManagement|Are you sure you want to cancel creating this comment?'),
- };
- }
- return {
- title: s__('DesignManagement|Cancel comment update confirmation'),
- okTitle: s__('DesignManagement|Cancel changes'),
- cancelTitle: s__('DesignManagement|Keep changes'),
- content: s__('DesignManagement|Are you sure you want to cancel changes to this comment?'),
- };
- },
buttonText() {
return this.isNewComment
- ? s__('DesignManagement|Comment')
- : s__('DesignManagement|Save comment');
- },
- markdownDocsPath() {
- return helpPagePath('user/markdown');
+ ? this.$options.i18n.newCommentButton
+ : this.$options.i18n.updateCommentButton;
},
shortDiscussionId() {
return isGid(this.discussionId) ? getIdFromGraphQLId(this.discussionId) : this.discussionId;
@@ -94,12 +85,30 @@ export default {
},
cancelComment() {
if (this.hasValue && this.formText !== this.value) {
- this.$refs.cancelCommentModal.show();
+ this.confirmCancelCommentModal();
} else {
this.$emit('cancel-form');
}
},
- confirmCancelCommentModal() {
+ async confirmCancelCommentModal() {
+ const msg = this.isNewComment
+ ? this.$options.i18n.cancelCreate
+ : this.$options.i18n.cancelUpdate;
+
+ const cancelBtn = this.isNewComment
+ ? this.$options.i18n.cancelBtnCreate
+ : this.$options.i18n.cancelBtnUpdate;
+
+ const confirmed = await confirmAction(msg, {
+ primaryBtnText: this.$options.i18n.primaryBtn,
+ cancelBtnText: cancelBtn,
+ primaryBtnVariant: 'danger',
+ });
+
+ if (!confirmed) {
+ return;
+ }
+
this.$emit('cancel-form');
this.autosaveDiscussion.reset();
},
@@ -126,7 +135,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:enable-autocomplete="true"
:textarea-value="value"
- :markdown-docs-path="markdownDocsPath"
+ :markdown-docs-path="$options.markdownDocsPath"
class="bordered-box"
>
<template #textarea>
@@ -171,15 +180,5 @@ export default {
>{{ __('Cancel') }}</gl-button
>
</div>
- <gl-modal
- ref="cancelCommentModal"
- ok-variant="danger"
- :title="modalSettings.title"
- :ok-title="modalSettings.okTitle"
- :cancel-title="modalSettings.cancelTitle"
- modal-id="cancel-comment-modal"
- @ok="confirmCancelCommentModal"
- >{{ modalSettings.content }}
- </gl-modal>
</form>
</template>
diff --git a/app/assets/javascripts/design_management/mixins/all_designs.js b/app/assets/javascripts/design_management/mixins/all_designs.js
index e92f8006a0d..b783ec43cd1 100644
--- a/app/assets/javascripts/design_management/mixins/all_designs.js
+++ b/app/assets/javascripts/design_management/mixins/all_designs.js
@@ -1,6 +1,6 @@
import { propertyOf } from 'lodash';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/flash';
import { s__ } from '~/locale';
import { DESIGNS_ROUTE_NAME } from '../router/constants';
import allVersionsMixin from './all_versions';
@@ -36,7 +36,7 @@ export default {
},
result() {
if (this.$route.query.version && !this.hasValidVersion) {
- createFlash({
+ createAlert({
message: s__(
'DesignManagement|Requested design version does not exist. Showing latest version instead',
),
@@ -44,11 +44,11 @@ export default {
this.$router.replace({ name: DESIGNS_ROUTE_NAME, query: { version: undefined } });
}
if (this.designCollection.copyState === 'ERROR') {
- createFlash({
+ createAlert({
message: s__(
'DesignManagement|There was an error moving your designs. Please upload your designs below.',
),
- type: FLASH_TYPES.WARNING,
+ variant: VARIANT_WARNING,
});
}
},
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 228ad637b9e..d4c177e2e5f 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -4,7 +4,7 @@ import { isNull } from 'lodash';
import Mousetrap from 'mousetrap';
import { ApolloMutation } from 'vue-apollo';
import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -250,7 +250,7 @@ export default {
onQueryError(message) {
// because we redirect user to /designs (the issue page),
// we want to create these flashes on the issue page
- createFlash({ message });
+ createAlert({ message });
this.$router.push({ name: this.$options.DESIGNS_ROUTE_NAME });
},
onError(message, e) {
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 07f7a19f7d4..fba73cd4bec 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -133,9 +133,13 @@ export default {
return this.designCollection && this.designCollection.copyState === 'IN_PROGRESS';
},
designDropzoneWrapperClass() {
- return this.isDesignListEmpty
- ? 'col-12'
- : 'gl-flex-direction-column col-md-6 col-lg-3 gl-mt-5';
+ if (!this.isDesignListEmpty) {
+ return 'gl-flex-direction-column col-md-6 col-lg-3 gl-mt-5';
+ }
+ if (this.showToolbar) {
+ return 'col-12 gl-mt-5';
+ }
+ return 'col-12';
},
},
mounted() {
diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js
index c8f445bfb88..cfec5828c85 100644
--- a/app/assets/javascripts/design_management/utils/cache_update.js
+++ b/app/assets/javascripts/design_management/utils/cache_update.js
@@ -2,7 +2,7 @@
import produce from 'immer';
import { differenceBy } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { extractCurrentDiscussion, extractDesign, extractDesigns } from './design_management_utils';
import {
ADD_IMAGE_DIFF_NOTE_ERROR,
@@ -234,7 +234,7 @@ export const deletePendingTodoFromStore = (store, todoMarkDone, query, queryVari
};
const onError = (data, message) => {
- createFlash({ message });
+ createAlert({ message });
throw new Error(data.errors);
};
@@ -283,7 +283,7 @@ export const updateStoreAfterUploadDesign = (store, data, query) => {
export const updateDesignsOnStoreAfterReorder = (store, data, query) => {
if (hasErrors(data)) {
- createFlash({ message: data.errors[0] });
+ createAlert({ message: data.errors[0] });
} else {
moveDesignInStore(store, data, query);
}
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 833fbb8789e..23eb470503e 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { merge } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import FilesCommentButton from './files_comment_button';
@@ -82,7 +82,7 @@ export default class Diff {
.get(link, { params })
.then(({ data }) => $target.parent().replaceWith(data))
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while loading diff'),
}),
);
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index f5c0776ca35..bc49464a560 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -11,7 +11,7 @@ import {
MR_COMMITS_NEXT_COMMIT,
MR_COMMITS_PREVIOUS_COMMIT,
} from '~/behaviors/shortcuts/keybindings';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
import { parseBoolean } from '~/lib/utils/common_utils';
@@ -471,8 +471,8 @@ export default {
},
fetchData(toggleTree = true) {
this.fetchDiffFilesMeta()
- .then(({ real_size }) => {
- this.diffFilesLength = parseInt(real_size, 10);
+ .then(({ real_size = 0 }) => {
+ this.diffFilesLength = parseInt(real_size, 10) || 0;
if (toggleTree) {
this.setTreeDisplay();
}
@@ -480,7 +480,7 @@ export default {
this.updateChangesTabCount();
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again!'),
});
});
@@ -495,7 +495,7 @@ export default {
this.setDiscussions();
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again!'),
});
});
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 0e5acd0928b..5a45797ed98 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -128,7 +128,7 @@ export default {
:img-src="authorAvatar"
:img-alt="authorName"
:img-size="32"
- class="avatar-cell d-none d-sm-block"
+ class="avatar-cell d-none d-sm-block gl-my-2 gl-mr-4"
/>
</div>
<div
@@ -172,7 +172,7 @@ export default {
v-if="commit.description_html"
v-safe-html:[$options.safeHtmlConfig]="commitDescription"
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
- class="commit-row-description gl-mb-3 gl-text-body"
+ class="commit-row-description gl-mb-3 gl-text-body gl-white-space-pre-line"
></pre>
</div>
</li>
diff --git a/app/assets/javascripts/diffs/components/commit_widget.vue b/app/assets/javascripts/diffs/components/commit_widget.vue
index b1a2b2a72ea..facfc553053 100644
--- a/app/assets/javascripts/diffs/components/commit_widget.vue
+++ b/app/assets/javascripts/diffs/components/commit_widget.vue
@@ -22,7 +22,7 @@ export default {
<template>
<div class="info-well mw-100 mx-0">
<div class="well-segment">
- <ul class="blob-commit-info">
+ <ul class="gl-list-style-none gl-m-0 gl-p-0">
<commit-item :commit="commit" :collapsible="collapsible" />
</ul>
</div>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 8a5325cf218..6104a304fbd 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -80,7 +80,7 @@ export default {
<template>
<div class="mr-version-controls">
- <div class="mr-version-menus-container content-block">
+ <div class="mr-version-menus-container gl-px-5 gl-pt-3 gl-pb-2">
<gl-button
v-if="hasChanges"
v-gl-tooltip.hover
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 3082ba0f16f..b2098b9e82d 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective, GlSafeHtmlDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mapActions } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__, sprintf } from '~/locale';
import { UNFOLD_COUNT, INLINE_DIFF_LINES_KEY } from '../constants';
import * as utils from '../store/utils';
@@ -92,7 +92,7 @@ export default {
) {
this.loadMoreLines({ endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers })
.catch(() => {
- createFlash({
+ createAlert({
message: s__('Diffs|Something went wrong while fetching diff lines.'),
});
})
@@ -224,6 +224,7 @@ export default {
<button
v-if="showExpandDown"
:title="s__('Diffs|Next 20 lines')"
+ :aria-label="s__('Diffs|Next 20 lines')"
:disabled="loading.down"
type="button"
class="js-unfold-down gl-rounded-0 gl-border-0 diff-line-expand-button"
@@ -235,6 +236,7 @@ export default {
<button
v-if="lineCountBetween !== -1 && lineCountBetween < 20"
:title="s__('Diffs|Expand all lines')"
+ :aria-label="s__('Diffs|Expand all lines')"
:disabled="loading.all"
type="button"
class="js-unfold-all gl-rounded-0 gl-border-0 diff-line-expand-button"
@@ -246,6 +248,7 @@ export default {
<button
v-if="showExpandUp"
:title="s__('Diffs|Previous 20 lines')"
+ :aria-label="s__('Diffs|Previous 20 lines')"
:disabled="loading.up"
type="button"
class="js-unfold gl-rounded-0 gl-border-0 diff-line-expand-button"
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index aec608007d5..422bf52a1fa 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -10,7 +10,7 @@ import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { IdState } from 'vendor/vue-virtual-scroller';
import DiffContent from 'jh_else_ce/diffs/components/diff_content.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import { diffViewerErrors } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils';
@@ -309,7 +309,7 @@ export default {
})
.catch(() => {
idState.isLoadingCollapsedDiff = false;
- createFlash({
+ createAlert({
message: this.$options.i18n.genericError,
});
});
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 705b43a222d..91c3df39e32 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -281,7 +281,7 @@ export default {
'gl-z-dropdown-menu!': idState.moreActionsShown,
'is-sidebar-moved': glFeatures.movedMrSidebar,
}"
- class="js-file-title file-title file-title-flex-parent"
+ class="js-file-title file-title file-title-flex-parent gl-border"
data-qa-selector="file_title_container"
:data-qa-file-name="filePath"
@click.self="handleToggleFile"
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index f610ac979ca..7732badde34 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -108,7 +108,7 @@ export const mapParallel = (content) => (line) => {
...left,
renderDiscussion: hasExpandedDiscussionOnLeft,
hasDraft: content.hasParallelDraftLeft(content.diffFile.file_hash, line),
- lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'left'),
+ lineDrafts: content.draftsForLine(content.diffFile.file_hash, line, 'left'),
hasCommentForm: left.hasForm,
isConflictMarker:
line.left.type === CONFLICT_MARKER_OUR || line.left.type === CONFLICT_MARKER_THEIR,
@@ -123,7 +123,7 @@ export const mapParallel = (content) => (line) => {
hasExpandedDiscussionOnRight && right.type && right.type !== EXPANDED_LINE_TYPE,
),
hasDraft: content.hasParallelDraftRight(content.diffFile.file_hash, line),
- lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'right'),
+ lineDrafts: content.draftsForLine(content.diffFile.file_hash, line, 'right'),
hasCommentForm: Boolean(right.hasForm && right.type && right.type !== EXPANDED_LINE_TYPE),
emptyCellClassMap: { conflict_their: line.left?.type === CONFLICT_OUR },
addCommentTooltip: addCommentTooltip(line.right),
@@ -145,7 +145,7 @@ export const mapParallel = (content) => (line) => {
lineCode: lineCode(line),
isMetaLineLeft: isMetaLine(left?.type),
isMetaLineRight: isMetaLine(right?.type),
- draftRowClasses: left?.lineDraft > 0 || right?.lineDraft > 0 ? '' : 'js-temp-notes-holder',
+ draftRowClasses: left?.hasDraft || right?.hasDraft ? '' : 'js-temp-notes-holder',
renderCommentRow,
commentRowClasses: hasDiscussions(left) || hasDiscussions(right) ? '' : 'js-temp-notes-holder',
};
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 91bf3283379..5ea118afe78 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -177,6 +177,12 @@ export default {
getCodeQualityLine(line) {
return (line.left ?? line.right)?.codequality?.[0]?.line;
},
+ lineDrafts(line, side) {
+ return (line[side]?.lineDrafts || []).filter((entry) => entry.isDraft);
+ },
+ lineHasDrafts(line, side) {
+ return this.lineDrafts(line, side).length > 0;
+ },
},
userColorScheme: window.gon.user_color_scheme,
};
@@ -297,19 +303,19 @@ export default {
class="diff-grid-drafts diff-tr notes_holder"
>
<div
- v-if="!inline || (line.left && line.left.lineDraft.isDraft)"
+ v-if="!inline || lineHasDrafts(line, 'left')"
class="diff-td notes-content parallel old"
>
- <div v-if="line.left && line.left.lineDraft.isDraft" class="content">
- <draft-note :draft="line.left.lineDraft" :line="line.left" />
+ <div v-for="draft in lineDrafts(line, 'left')" :key="draft.id" class="content">
+ <draft-note :draft="draft" :line="line.left" />
</div>
</div>
<div
- v-if="!inline || (line.right && line.right.lineDraft.isDraft)"
+ v-if="!inline || lineHasDrafts(line, 'right')"
class="diff-td notes-content parallel new"
>
- <div v-if="line.right && line.right.lineDraft.isDraft" class="content">
- <draft-note :draft="line.right.lineDraft" :line="line.right" />
+ <div v-for="draft in lineDrafts(line, 'right')" :key="draft.id" class="content">
+ <draft-note :draft="draft" :line="line.right" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/mixins/draft_comments.js b/app/assets/javascripts/diffs/mixins/draft_comments.js
index 693b4a84694..d41bb160e96 100644
--- a/app/assets/javascripts/diffs/mixins/draft_comments.js
+++ b/app/assets/javascripts/diffs/mixins/draft_comments.js
@@ -5,7 +5,7 @@ export default {
...mapGetters('batchComments', [
'shouldRenderDraftRow',
'shouldRenderParallelDraftRow',
- 'draftForLine',
+ 'draftsForLine',
'draftsForFile',
'hasParallelDraftLeft',
'hasParallelDraftRight',
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 5e74a7206b3..5234be44b05 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -5,7 +5,7 @@ import {
historyPushState,
scrollToElement,
} from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { diffViewerModes } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
@@ -202,6 +202,7 @@ export const fetchDiffFilesMeta = ({ commit, state }) => {
const worker = new TreeWorker();
const urlParams = {
view: 'inline',
+ w: state.showWhitespace ? '0' : '1',
};
commit(types.SET_LOADING, true);
@@ -246,7 +247,7 @@ export const fetchCoverageFiles = ({ commit, state }) => {
}
},
errorCallback: () =>
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again!'),
}),
});
@@ -509,7 +510,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
.then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
.catch(() =>
- createFlash({
+ createAlert({
message: s__('MergeRequests|Saving the comment failed'),
}),
);
@@ -619,7 +620,7 @@ export const cacheTreeListWidth = (_, size) => {
export const receiveFullDiffError = ({ commit }, filePath) => {
commit(types.RECEIVE_FULL_DIFF_ERROR, filePath);
- createFlash({
+ createAlert({
message: s__('MergeRequest|Error loading full diff. Please try again.'),
});
};
@@ -757,7 +758,7 @@ export const setSuggestPopoverDismissed = ({ commit, state }) =>
commit(types.SET_SHOW_SUGGEST_POPOVER);
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__('MergeRequest|Error dismissing suggestion popover. Please try again.'),
});
});
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
index bc3cb163c39..999e91eed19 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -1,7 +1,7 @@
import { KeyMod, KeyCode } from 'monaco-editor';
import { debounce } from 'lodash';
import { BLOB_PREVIEW_ERROR } from '~/blob_edit/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import syntaxHighlight from '~/syntax_highlight';
@@ -152,7 +152,7 @@ export class EditorMarkdownPreviewExtension {
syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight'));
previewEl.style.display = 'block';
})
- .catch(() => createFlash(BLOB_PREVIEW_ERROR));
+ .catch(() => createAlert(BLOB_PREVIEW_ERROR));
}
setupPreviewAction(instance) {
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 848ba7dbeef..e56932a9a31 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -1,7 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gitlab.com/.gitlab-ci.yml",
- "title": "Gitlab CI configuration",
"markdownDescription": "Gitlab has a built-in solution for doing CI called Gitlab CI. It is configured by supplying a file called `.gitlab-ci.yml`, which will list all the jobs that are going to run for the project. A full list of all options can be found [here](https://docs.gitlab.com/ee/ci/yaml). [Learn More](https://docs.gitlab.com/ee/ci/index.html).",
"type": "object",
"properties": {
@@ -9,34 +8,74 @@
"type": "string",
"format": "uri"
},
- "image": { "$ref": "#/definitions/image" },
- "services": { "$ref": "#/definitions/services" },
- "before_script": { "$ref": "#/definitions/before_script" },
- "after_script": { "$ref": "#/definitions/after_script" },
- "variables": { "$ref": "#/definitions/globalVariables" },
- "cache": { "$ref": "#/definitions/cache" },
- "!reference": {"$ref" : "#/definitions/!reference"},
+ "image": {
+ "$ref": "#/definitions/image"
+ },
+ "services": {
+ "$ref": "#/definitions/services"
+ },
+ "before_script": {
+ "$ref": "#/definitions/before_script"
+ },
+ "after_script": {
+ "$ref": "#/definitions/after_script"
+ },
+ "variables": {
+ "$ref": "#/definitions/globalVariables"
+ },
+ "cache": {
+ "$ref": "#/definitions/cache"
+ },
+ "!reference": {
+ "$ref": "#/definitions/!reference"
+ },
"default": {
"type": "object",
"properties": {
- "after_script": { "$ref": "#/definitions/after_script" },
- "artifacts": { "$ref": "#/definitions/artifacts" },
- "before_script": { "$ref": "#/definitions/before_script" },
- "cache": { "$ref": "#/definitions/cache" },
- "image": { "$ref": "#/definitions/image" },
- "interruptible": { "$ref": "#/definitions/interruptible" },
- "retry": { "$ref": "#/definitions/retry" },
- "services": { "$ref": "#/definitions/services" },
- "tags": { "$ref": "#/definitions/tags" },
- "timeout": { "$ref": "#/definitions/timeout" },
- "!reference": {"$ref" : "#/definitions/!reference"}
+ "after_script": {
+ "$ref": "#/definitions/after_script"
+ },
+ "artifacts": {
+ "$ref": "#/definitions/artifacts"
+ },
+ "before_script": {
+ "$ref": "#/definitions/before_script"
+ },
+ "cache": {
+ "$ref": "#/definitions/cache"
+ },
+ "image": {
+ "$ref": "#/definitions/image"
+ },
+ "interruptible": {
+ "$ref": "#/definitions/interruptible"
+ },
+ "retry": {
+ "$ref": "#/definitions/retry"
+ },
+ "services": {
+ "$ref": "#/definitions/services"
+ },
+ "tags": {
+ "$ref": "#/definitions/tags"
+ },
+ "timeout": {
+ "$ref": "#/definitions/timeout"
+ },
+ "!reference": {
+ "$ref": "#/definitions/!reference"
+ }
},
"additionalProperties": false
},
"stages": {
"type": "array",
"markdownDescription": "Groups jobs into stages. All jobs in one stage must complete before next stage is executed. Defaults to ['build', 'test', 'deploy']. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#stages).",
- "default": ["build", "test", "deploy"],
+ "default": [
+ "build",
+ "test",
+ "deploy"
+ ],
"items": {
"type": "string"
},
@@ -46,10 +85,14 @@
"include": {
"markdownDescription": "Can be `IncludeItem` or `IncludeItem[]`. Each `IncludeItem` will be a string, or an object with properties for the method if including external YAML file. The external content will be fetched, included and evaluated along the `.gitlab-ci.yml`. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#include).",
"oneOf": [
- { "$ref": "#/definitions/include_item" },
+ {
+ "$ref": "#/definitions/include_item"
+ },
{
"type": "array",
- "items": { "$ref": "#/definitions/include_item" }
+ "items": {
+ "$ref": "#/definitions/include_item"
+ }
}
]
},
@@ -60,21 +103,41 @@
"workflow": {
"type": "object",
"properties": {
+ "name": { "$ref": "#/definitions/workflowName" },
"rules": {
"type": "array",
"items": {
"anyOf": [
- {"type": "object"},
- {"type": "array", "minLength": 1, "items": { "type": "string" }}
+ {
+ "type": "object"
+ },
+ {
+ "type": "array",
+ "minLength": 1,
+ "items": {
+ "type": "string"
+ }
+ }
],
"properties": {
- "if": { "$ref": "#/definitions/if" },
- "changes": { "$ref": "#/definitions/changes" },
- "exists": { "$ref": "#/definitions/exists" },
- "variables": { "$ref": "#/definitions/variables" },
+ "if": {
+ "$ref": "#/definitions/if"
+ },
+ "changes": {
+ "$ref": "#/definitions/changes"
+ },
+ "exists": {
+ "$ref": "#/definitions/exists"
+ },
+ "variables": {
+ "$ref": "#/definitions/variables"
+ },
"when": {
"type": "string",
- "enum": ["always", "never"]
+ "enum": [
+ "always",
+ "never"
+ ]
}
},
"additionalProperties": false
@@ -87,8 +150,12 @@
"^[.]": {
"description": "Hidden keys.",
"anyOf": [
- { "$ref": "#/definitions/job_template" },
- { "description": "Arbitrary YAML anchor." }
+ {
+ "$ref": "#/definitions/job_template"
+ },
+ {
+ "description": "Arbitrary YAML anchor."
+ }
]
}
},
@@ -135,15 +202,21 @@
"default": "on_success",
"oneOf": [
{
- "enum": ["on_success"],
+ "enum": [
+ "on_success"
+ ],
"description": "Upload artifacts only when the job succeeds (this is the default)."
},
{
- "enum": ["on_failure"],
+ "enum": [
+ "on_failure"
+ ],
"description": "Upload artifacts only when the job fails."
},
{
- "enum": ["always"],
+ "enum": [
+ "always"
+ ],
"description": "Upload artifacts regardless of job status."
}
]
@@ -181,7 +254,9 @@
"properties": {
"coverage_format": {
"description": "Code coverage format used by the test framework.",
- "enum": ["cobertura"]
+ "enum": [
+ "cobertura"
+ ]
},
"path": {
"description": "Path to the coverage report file that should be parsed.",
@@ -285,18 +360,22 @@
"format": "uri-reference",
"pattern": "\\.ya?ml$"
},
- "rules": { "$ref": "#/definitions/rules" }
+ "rules": {
+ "$ref": "#/definitions/rules"
+ }
},
- "required": ["local"]
+ "required": [
+ "local"
+ ]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"project": {
- "description": "Path to the project, e.g. `group/project`, or `group/sub-group/project`.",
+ "description": "Path to the project, e.g. `group/project`, or `group/sub-group/project` [Learn more](https://docs.gitlab.com/ee/ci/yaml/index.html#includefile).",
"type": "string",
- "pattern": "\\S/\\S|\\$(\\S+)"
+ "pattern": "(?:\\S/\\S|\\$\\S+)"
},
"ref": {
"description": "Branch/Tag/Commit-hash for the target project.",
@@ -320,7 +399,10 @@
]
}
},
- "required": ["project", "file"]
+ "required": [
+ "project",
+ "file"
+ ]
},
{
"type": "object",
@@ -333,7 +415,9 @@
"pattern": "\\.ya?ml$"
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
{
"type": "object",
@@ -346,7 +430,9 @@
"pattern": "^https?://.+\\.ya?ml$"
}
},
- "required": ["remote"]
+ "required": [
+ "remote"
+ ]
}
]
},
@@ -407,7 +493,16 @@
]
}
},
- "required": ["name"]
+ "required": [
+ "name"
+ ]
+ },
+ {
+ "type": "array",
+ "minLength": 1,
+ "items": {
+ "type": "string"
+ }
}
],
"markdownDescription": "Specifies the docker image to use for the job or globally for all jobs. Job configuration takes precedence over global setting. Requires a certain kind of Gitlab runner executor. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#image)."
@@ -481,7 +576,9 @@
"minLength": 1
}
},
- "required": ["name"]
+ "required": [
+ "name"
+ ]
}
]
}
@@ -505,20 +602,37 @@
"engine": {
"type": "object",
"properties": {
- "name": { "type": "string" },
- "path": { "type": "string" }
+ "name": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ }
},
- "required": ["name", "path"]
+ "required": [
+ "name",
+ "path"
+ ]
+ },
+ "path": {
+ "type": "string"
},
- "path": { "type": "string" },
- "field": { "type": "string" }
+ "field": {
+ "type": "string"
+ }
},
- "required": ["engine", "path", "field"]
+ "required": [
+ "engine",
+ "path",
+ "field"
+ ]
}
]
}
},
- "required": ["vault"]
+ "required": [
+ "vault"
+ ]
}
},
"before_script": {
@@ -564,45 +678,77 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "if": { "$ref": "#/definitions/if" },
- "changes": { "$ref": "#/definitions/changes" },
- "exists": { "$ref": "#/definitions/exists" },
- "variables": { "$ref": "#/definitions/variables" },
- "when": { "$ref": "#/definitions/when" },
- "start_in": { "$ref": "#/definitions/start_in" },
- "allow_failure": { "$ref": "#/definitions/allow_failure" }
+ "if": {
+ "$ref": "#/definitions/if"
+ },
+ "changes": {
+ "$ref": "#/definitions/changes"
+ },
+ "exists": {
+ "$ref": "#/definitions/exists"
+ },
+ "variables": {
+ "$ref": "#/definitions/variables"
+ },
+ "when": {
+ "$ref": "#/definitions/when"
+ },
+ "start_in": {
+ "$ref": "#/definitions/start_in"
+ },
+ "allow_failure": {
+ "$ref": "#/definitions/allow_failure"
+ }
}
},
- {"type": "string", "minLength": 1},
- {"type": "array", "minLength": 1, "items": { "type": "string" }}
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "minLength": 1,
+ "items": {
+ "type": "string"
+ }
+ }
]
}
},
+ "workflowName": {
+ "type": "string",
+ "markdownDescription": "Defines the pipeline name. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#workflowname).",
+ "minLength": 1,
+ "maxLength": 255
+ },
"globalVariables": {
- "markdownDescription": "Defines environment variables globally. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off. You can use the value and description keywords to define variables that are prefilled when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
- "anyOf": [
- {"type": "object"},
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- ],
- "additionalProperties": {
- "anyOf": [
- {"type": ["string", "integer", "array"]},
- {
- "type": "object",
- "properties": {
- "value": { "type": "string" },
- "description": {
- "type": "string",
- "description": "Explains what the variable is used for, what the acceptable values are."
- }
+ "markdownDescription": "Defines default variables for all jobs. Job level property overrides global variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "oneOf": [
+ {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string",
+ "markdownDescription": "Explains what the variable is used for, what the acceptable values are. Variables with `description` are prefilled when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variablesdescription)."
+ }
+ },
+ "additionalProperties": false
}
- }
- ]
+ ]
+ },
+ "additionalProperties": false
}
},
"if": {
@@ -615,7 +761,9 @@
{
"type": "object",
"additionalProperties": false,
- "required": ["paths"],
+ "required": [
+ "paths"
+ ],
"properties": {
"paths": {
"type": "array",
@@ -646,21 +794,17 @@
}
},
"variables": {
- "markdownDescription": "Defines environment variables for specific jobs. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#rulesvariables).",
- "anyOf": [
- {
- "type": "object",
- "additionalProperties": {
- "type": ["string", "integer", "array"]
- }
+ "markdownDescription": "Defines environment variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "type": [
+ "string",
+ "number"
+ ]
},
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- ]
+ "additionalProperties": false
+ }
},
"timeout": {
"type": "string",
@@ -684,7 +828,9 @@
"description": "Exit code that are not considered failure. The job fails for any other exit code.",
"type": "object",
"additionalProperties": false,
- "required": ["exit_codes"],
+ "required": [
+ "exit_codes"
+ ],
"properties": {
"exit_codes": {
"type": "integer"
@@ -695,7 +841,9 @@
"description": "You can list which exit codes are not considered failures. The job fails for any other exit code.",
"type": "object",
"additionalProperties": false,
- "required": ["exit_codes"],
+ "required": [
+ "exit_codes"
+ ],
"properties": {
"exit_codes": {
"type": "array",
@@ -714,27 +862,39 @@
"default": "on_success",
"oneOf": [
{
- "enum": ["on_success"],
+ "enum": [
+ "on_success"
+ ],
"description": "Execute job only when all jobs from prior stages succeed."
},
{
- "enum": ["on_failure"],
+ "enum": [
+ "on_failure"
+ ],
"description": "Execute job when at least one job from prior stages fails."
},
{
- "enum": ["always"],
+ "enum": [
+ "always"
+ ],
"description": "Execute job regardless of the status from prior stages."
},
{
- "enum": ["manual"],
+ "enum": [
+ "manual"
+ ],
"markdownDescription": "Execute the job manually from Gitlab UI or API. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when)."
},
{
- "enum": ["delayed"],
+ "enum": [
+ "delayed"
+ ],
"markdownDescription": "Execute a job after the time limit in 'start_in' expires. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when)."
},
{
- "enum": ["never"],
+ "enum": [
+ "never"
+ ],
"description": "Never execute the job."
}
]
@@ -746,15 +906,21 @@
"default": "on_success",
"oneOf": [
{
- "enum": ["on_success"],
+ "enum": [
+ "on_success"
+ ],
"description": "Save the cache only when the job succeeds."
},
{
- "enum": ["on_failure"],
+ "enum": [
+ "on_failure"
+ ],
"description": "Save the cache only when the job fails. "
},
{
- "enum": ["always"],
+ "enum": [
+ "always"
+ ],
"description": "Always save the cache. "
}
]
@@ -806,15 +972,21 @@
"default": "pull-push",
"oneOf": [
{
- "enum": ["pull"],
+ "enum": [
+ "pull"
+ ],
"description": "Pull will download cache but skip uploading after job completes."
},
{
- "enum": ["push"],
+ "enum": [
+ "push"
+ ],
"description": "Push will skip downloading cache and always recreate cache after job completes."
},
{
- "enum": ["pull-push"],
+ "enum": [
+ "pull-push"
+ ],
"description": "Pull-push will both download cache at job start and upload cache on job success."
}
]
@@ -829,39 +1001,57 @@
{
"oneOf": [
{
- "enum": ["branches"],
+ "enum": [
+ "branches"
+ ],
"description": "When a branch is pushed."
},
{
- "enum": ["tags"],
+ "enum": [
+ "tags"
+ ],
"description": "When a tag is pushed."
},
{
- "enum": ["api"],
+ "enum": [
+ "api"
+ ],
"description": "When a pipeline has been triggered by a second pipelines API (not triggers API)."
},
{
- "enum": ["external"],
+ "enum": [
+ "external"
+ ],
"description": "When using CI services other than Gitlab"
},
{
- "enum": ["pipelines"],
+ "enum": [
+ "pipelines"
+ ],
"description": "For multi-project triggers, created using the API with 'CI_JOB_TOKEN'."
},
{
- "enum": ["pushes"],
+ "enum": [
+ "pushes"
+ ],
"description": "Pipeline is triggered by a `git push` by the user"
},
{
- "enum": ["schedules"],
+ "enum": [
+ "schedules"
+ ],
"description": "For scheduled pipelines."
},
{
- "enum": ["triggers"],
+ "enum": [
+ "triggers"
+ ],
"description": "For pipelines created using a trigger token."
},
{
- "enum": ["web"],
+ "enum": [
+ "web"
+ ],
"description": "For pipelines created using *Run pipeline* button in Gitlab UI (under your project's *Pipelines*)."
}
]
@@ -889,7 +1079,9 @@
"$ref": "#/definitions/filter_refs"
},
"kubernetes": {
- "enum": ["active"],
+ "enum": [
+ "active"
+ ],
"description": "Filter job based on if Kubernetes integration is active."
},
"variables": {
@@ -913,16 +1105,22 @@
"retry": {
"markdownDescription": "Retry a job if it fails. Can be a simple integer or object definition. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#retry).",
"oneOf": [
- { "$ref": "#/definitions/retry_max" },
+ {
+ "$ref": "#/definitions/retry_max"
+ },
{
"type": "object",
"additionalProperties": false,
"properties": {
- "max": { "$ref": "#/definitions/retry_max" },
+ "max": {
+ "$ref": "#/definitions/retry_max"
+ },
"when": {
"markdownDescription": "Either a single or array of error types to trigger job retry. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#retrywhen).",
"oneOf": [
- { "$ref": "#/definitions/retry_errors" },
+ {
+ "$ref": "#/definitions/retry_errors"
+ },
{
"type": "array",
"items": {
@@ -1005,21 +1203,39 @@
},
"job": {
"allOf": [
- { "$ref": "#/definitions/job_template" }
+ {
+ "$ref": "#/definitions/job_template"
+ }
]
},
"job_template": {
"type": "object",
"additionalProperties": false,
"properties": {
- "image": { "$ref": "#/definitions/image" },
- "services": { "$ref": "#/definitions/services" },
- "before_script": { "$ref": "#/definitions/before_script" },
- "after_script": { "$ref": "#/definitions/after_script" },
- "rules": { "$ref": "#/definitions/rules" },
- "variables": { "$ref": "#/definitions/variables" },
- "cache": { "$ref": "#/definitions/cache" },
- "secrets": { "$ref": "#/definitions/secrets" },
+ "image": {
+ "$ref": "#/definitions/image"
+ },
+ "services": {
+ "$ref": "#/definitions/services"
+ },
+ "before_script": {
+ "$ref": "#/definitions/before_script"
+ },
+ "after_script": {
+ "$ref": "#/definitions/after_script"
+ },
+ "rules": {
+ "$ref": "#/definitions/rules"
+ },
+ "variables": {
+ "$ref": "#/definitions/variables"
+ },
+ "cache": {
+ "$ref": "#/definitions/cache"
+ },
+ "secrets": {
+ "$ref": "#/definitions/secrets"
+ },
"script": {
"markdownDescription": "Shell scripts executed by the Runner. The only required property of jobs. Be careful with special characters (e.g. `:`, `{`, `}`, `&`) and use single or double quotes to avoid issues. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#script)",
"oneOf": [
@@ -1047,9 +1263,20 @@
]
},
"stage": {
- "type": "string",
"description": "Define what stage the job will run in.",
- "minLength": 1
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "minLength": 1,
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
},
"only": {
"$ref": "#/definitions/filter",
@@ -1092,7 +1319,9 @@
"type": "boolean"
}
},
- "required": ["job"]
+ "required": [
+ "job"
+ ]
},
{
"type": "object",
@@ -1108,7 +1337,10 @@
"type": "boolean"
}
},
- "required": ["job", "pipeline"]
+ "required": [
+ "job",
+ "pipeline"
+ ]
},
{
"type": "object",
@@ -1127,7 +1359,11 @@
"type": "boolean"
}
},
- "required": ["job", "project", "ref"]
+ "required": [
+ "job",
+ "project",
+ "ref"
+ ]
}
]
}
@@ -1164,7 +1400,9 @@
"environment": {
"description": "Used to associate environment metadata with a deploy. Environment can have a name and URL attached to it, and will be displayed under /environments under the project.",
"oneOf": [
- { "type": "string" },
+ {
+ "type": "string"
+ },
{
"type": "object",
"additionalProperties": false,
@@ -1185,7 +1423,13 @@
"description": "The name of a job to execute when the environment is about to be stopped."
},
"action": {
- "enum": ["start", "prepare", "stop", "verify", "access"],
+ "enum": [
+ "start",
+ "prepare",
+ "stop",
+ "verify",
+ "access"
+ ],
"description": "Specifies what this job will do. 'start' (default) indicates the job will start the deployment. 'prepare'/'verify'/'access' indicates this will not affect the deployment. 'stop' indicates this will stop the deployment.",
"default": "start"
},
@@ -1216,7 +1460,9 @@
]
}
},
- "required": ["name"]
+ "required": [
+ "name"
+ ]
}
]
},
@@ -1296,15 +1542,23 @@
]
}
},
- "required": ["name", "url"]
+ "required": [
+ "name",
+ "url"
+ ]
},
"minItems": 1
}
},
- "required": ["links"]
+ "required": [
+ "links"
+ ]
}
},
- "required": ["tag_name", "description"]
+ "required": [
+ "tag_name",
+ "description"
+ ]
},
"coverage": {
"type": "string",
@@ -1335,14 +1589,20 @@
"type": "object",
"description": "Defines environment variables for specific job.",
"additionalProperties": {
- "type": ["string", "number", "array"]
+ "type": [
+ "string",
+ "number",
+ "array"
+ ]
}
},
"maxItems": 50
}
},
"additionalProperties": false,
- "required": ["matrix"]
+ "required": [
+ "matrix"
+ ]
}
]
},
@@ -1364,7 +1624,7 @@
"project": {
"description": "Path to the project, e.g. `group/project`, or `group/sub-group/project`.",
"type": "string",
- "pattern": "\\S/\\S"
+ "pattern": "(?:\\S/\\S|\\$\\S+)"
},
"branch": {
"description": "The branch name that a downstream pipeline will use",
@@ -1373,7 +1633,9 @@
"strategy": {
"description": "You can mirror the pipeline status from the triggered pipeline to the source bridge job by using strategy: depend",
"type": "string",
- "enum": ["depend"]
+ "enum": [
+ "depend"
+ ]
},
"forward": {
"description": "Specify what to forward to the downstream pipeline.",
@@ -1393,9 +1655,13 @@
}
}
},
- "required": ["project"],
+ "required": [
+ "project"
+ ],
"dependencies": {
- "branch": ["project"]
+ "branch": [
+ "project"
+ ]
}
},
{
@@ -1456,7 +1722,10 @@
"type": "string"
}
},
- "required": ["artifact", "job"]
+ "required": [
+ "artifact",
+ "job"
+ ]
},
{
"type": "object",
@@ -1465,7 +1734,7 @@
"project": {
"description": "Path to another private project under the same GitLab instance, like `group/project` or `group/sub-group/project`.",
"type": "string",
- "pattern": "\\S/\\S"
+ "pattern": "(?:\\S/\\S|\\$\\S+)"
},
"ref": {
"description": "Branch/Tag/Commit hash for the target project.",
@@ -1479,7 +1748,10 @@
"pattern": "\\.ya?ml$"
}
},
- "required": ["project", "file"]
+ "required": [
+ "project",
+ "file"
+ ]
}
]
}
@@ -1489,7 +1761,9 @@
"strategy": {
"description": "You can mirror the pipeline status from the triggered pipeline to the source bridge job by using strategy: depend",
"type": "string",
- "enum": ["depend"]
+ "enum": [
+ "depend"
+ ]
},
"forward": {
"description": "Specify what to forward to the downstream pipeline.",
@@ -1511,9 +1785,9 @@
}
},
{
- "markdownDescription": "Path to the project, e.g. `group/project`, or `group/sub-group/project`. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html#define-multi-project-pipelines-in-your-gitlab-ciyml-file).",
+ "markdownDescription": "Path to the project, e.g. `group/project`, or `group/sub-group/project`. [Learn More](https://docs.gitlab.com/ee/ci/yaml/index.html#trigger).",
"type": "string",
- "pattern": "\\S/\\S"
+ "pattern": "(?:\\S/\\S|\\$\\S+)"
}
]
},
@@ -1550,7 +1824,9 @@
"variables": {
"markdownDescription": "Whether to inherit all globally-defined variables or not. Or subset of inherited variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#inheritvariables).",
"oneOf": [
- { "type": "boolean" },
+ {
+ "type": "boolean"
+ },
{
"type": "array",
"items": {
@@ -1566,15 +1842,24 @@
"oneOf": [
{
"properties": {
- "when": { "enum": ["delayed"] }
+ "when": {
+ "enum": [
+ "delayed"
+ ]
+ }
},
- "required": ["when", "start_in"]
+ "required": [
+ "when",
+ "start_in"
+ ]
},
{
"properties": {
"when": {
"not": {
- "enum": ["delayed"]
+ "enum": [
+ "delayed"
+ ]
}
}
}
@@ -1583,10 +1868,23 @@
},
"tags": {
"type": "array",
+ "minLength": 1,
"markdownDescription": "Used to select runners from the list of available runners. A runner must have all tags listed here to run the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#tags).",
"items": {
- "type": "string"
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "minLength": 1,
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
}
}
}
-}
+} \ No newline at end of file
diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue
index 3173c2bd644..78e1b8d5cb2 100644
--- a/app/assets/javascripts/environments/components/delete_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
import deleteEnvironmentMutation from '../graphql/mutations/delete_environment.mutation.graphql';
@@ -65,11 +65,11 @@ export default {
.then(({ data }) => {
const [message] = data?.deleteEvironment?.errors ?? [];
if (message) {
- createFlash({ message });
+ createAlert({ message });
}
})
.catch((error) =>
- createFlash({
+ createAlert({
message: s__(
'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
),
diff --git a/app/assets/javascripts/environments/components/deployment.vue b/app/assets/javascripts/environments/components/deployment.vue
index 3475b38c8c9..b00a0777a03 100644
--- a/app/assets/javascripts/environments/components/deployment.vue
+++ b/app/assets/javascripts/environments/components/deployment.vue
@@ -10,7 +10,7 @@ import {
import { __, s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import deploymentDetails from '../graphql/queries/deployment_details.query.graphql';
import DeploymentStatusBadge from './deployment_status_badge.vue';
import Commit from './commit.vue';
@@ -119,7 +119,7 @@ export default {
return data?.project?.deployment?.tags;
},
error(error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.LOAD_ERROR_MESSAGE,
captureError: true,
error,
diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue
index 96742a11ebb..901d0f5b34d 100644
--- a/app/assets/javascripts/environments/components/edit_environment.vue
+++ b/app/assets/javascripts/environments/components/edit_environment.vue
@@ -1,5 +1,5 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import EnvironmentForm from './environment_form.vue';
@@ -39,7 +39,7 @@ export default {
.then(({ data: { path } }) => visitUrl(path))
.catch((error) => {
const message = error.response.data.message[0];
- createFlash({ message });
+ createAlert({ message });
this.loading = false;
});
},
diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue
index 563fa6c96fb..e40c37b5095 100644
--- a/app/assets/javascripts/environments/components/empty_state.vue
+++ b/app/assets/javascripts/environments/components/empty_state.vue
@@ -1,9 +1,14 @@
<script>
+import { GlEmptyState, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
import { ENVIRONMENTS_SCOPE } from '../constants';
export default {
- name: 'EnvironmentsEmptyState',
+ components: {
+ GlEmptyState,
+ GlLink,
+ },
+ inject: ['newEnvironmentPath'],
props: {
helpPath: {
type: String,
@@ -13,10 +18,23 @@ export default {
type: String,
required: true,
},
+ hasTerm: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
title() {
- return this.$options.i18n.title[this.scope];
+ return this.hasTerm
+ ? this.$options.i18n.searchingTitle
+ : this.$options.i18n.title[this.scope];
+ },
+ content() {
+ return this.hasTerm ? this.$options.i18n.searchingContent : this.$options.i18n.content;
+ },
+ buttonText() {
+ return this.hasTerm ? this.$options.i18n.newEnvironmentButtonLabel : '';
},
},
i18n: {
@@ -27,20 +45,21 @@ export default {
content: s__(
'Environments|Environments are places where code gets deployed, such as staging or production.',
),
+ searchingTitle: s__('Environments|No results found'),
+ searchingContent: s__('Environments|Edit your search and try again'),
link: s__('Environments|How do I create an environment?'),
+ newEnvironmentButtonLabel: s__('Environments|New environment'),
},
};
</script>
<template>
- <div class="empty-state">
- <div class="text-content">
- <h4 class="js-blank-state-title">
- {{ title }}
- </h4>
- <p>
- {{ $options.i18n.content }}
- <a :href="helpPath"> {{ $options.i18n.link }} </a>
- </p>
- </div>
- </div>
+ <gl-empty-state :primary-button-text="buttonText" :primary-button-link="newEnvironmentPath">
+ <template #title>
+ <h4>{{ title }}</h4>
+ </template>
+ <template #description>
+ <p>{{ content }}</p>
+ <gl-link v-if="!hasTerm" :href="helpPath">{{ $options.i18n.link }}</gl-link>
+ </template>
+ </gl-empty-state>
</template>
diff --git a/app/assets/javascripts/environments/components/enable_review_app_modal.vue b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
index 6343fe8702a..420ad3d9c42 100644
--- a/app/assets/javascripts/environments/components/enable_review_app_modal.vue
+++ b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
@@ -1,18 +1,19 @@
<script>
-import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlModal, GlSprintf, GlIcon, GlPopover } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { REVIEW_APP_MODAL_I18N as i18n } from '../constants';
export default {
components: {
GlLink,
GlModal,
GlSprintf,
+ GlIcon,
+ GlPopover,
ModalCopyButton,
},
- inject: ['defaultBranchName'],
model: {
prop: 'visible',
event: 'change',
@@ -28,25 +29,6 @@ export default {
default: false,
},
},
- instructionText: {
- step1: s__(
- 'EnableReviewApp|%{stepStart}Step 1%{stepEnd}. Ensure you have Kubernetes set up and have a base domain for your %{linkStart}cluster%{linkEnd}.',
- ),
- step2: s__('EnableReviewApp|%{stepStart}Step 2%{stepEnd}. Copy the following snippet:'),
- step3: s__(
- `EnableReviewApp|%{stepStart}Step 3%{stepEnd}. Add it to the project %{linkStart}gitlab-ci.yml%{linkEnd} file.`,
- ),
- step4: s__(
- `EnableReviewApp|%{stepStart}Step 4 (optional)%{stepEnd}. Enable Visual Reviews by following the %{linkStart}setup instructions%{linkEnd}.`,
- ),
- },
- modalInfo: {
- closeText: s__('EnableReviewApp|Close'),
- copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
- title: s__('ReviewApp|Enable Review App'),
- },
- visualReviewsDocs: helpPagePath('ci/review_apps/index.md', { anchor: 'visual-reviews' }),
- connectClusterDocs: helpPagePath('user/clusters/agent/index'),
data() {
const modalInfoCopyId = uniqueId('enable-review-app-copy-string-');
@@ -57,81 +39,99 @@ export default {
return `deploy_review:
stage: deploy
script:
- - echo "Deploy a review app"
+ - echo "Add script here that deploys the code to your infrastructure"
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
- only:
- - branches
- except:
- - ${this.defaultBranchName}`;
+ rules:
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"`;
+ },
+ },
+ methods: {
+ commaOrPeriod(index, length) {
+ return index + 1 === length ? '.' : ',';
},
},
+ i18n,
+ configuringReviewAppsPath: helpPagePath('ci/review_apps/index.md', {
+ anchor: 'configuring-review-apps',
+ }),
+ reviewAppsExamplesPath: helpPagePath('ci/review_apps/index.md', {
+ anchor: 'review-apps-examples',
+ }),
};
</script>
<template>
<gl-modal
:visible="visible"
:modal-id="modalId"
- :title="$options.modalInfo.title"
+ :title="$options.i18n.title"
static
size="lg"
- ok-only
- ok-variant="light"
- :ok-title="$options.modalInfo.closeText"
+ hide-footer
@change="$emit('change', $event)"
>
+ <p>{{ $options.i18n.intro }}</p>
<p>
- <gl-sprintf :message="$options.instructionText.step1">
- <template #step="{ content }">
- <strong>{{ content }}</strong>
- </template>
- <template #link="{ content }">
- <gl-link :href="$options.connectClusterDocs" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
+ <strong>{{ $options.i18n.instructions.title }}</strong>
</p>
- <div>
- <p>
- <gl-sprintf :message="$options.instructionText.step2">
- <template #step="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- </p>
- <div class="gl-display-flex align-items-start">
- <pre :id="modalInfoCopyId" class="gl-w-full" data-testid="enable-review-app-copy-string">
- {{ modalInfoCopyStr }} </pre
- >
- <modal-copy-button
- :title="$options.modalInfo.copyToClipboardText"
- :modal-id="modalId"
- css-classes="border-0"
- :target="`#${modalInfoCopyId}`"
- />
- </div>
+ <div class="gl-mb-6">
+ <ol class="gl-px-6">
+ <li>
+ {{ $options.i18n.instructions.step1 }}
+ <gl-icon
+ ref="informationIcon"
+ name="information-o"
+ class="gl-text-blue-600 gl-hover-cursor-pointer"
+ />
+ <gl-popover
+ :target="() => $refs.informationIcon.$el"
+ :title="$options.i18n.staticSitePopover.title"
+ triggers="hover focus"
+ >
+ {{ $options.i18n.staticSitePopover.body }}
+ </gl-popover>
+ </li>
+ <li>{{ $options.i18n.instructions.step2 }}</li>
+ <li>
+ {{ $options.i18n.instructions.step3 }}
+ <ul class="gl-px-4 gl-py-2">
+ <li>{{ $options.i18n.instructions.step3a }}</li>
+ <li>
+ <gl-sprintf :message="$options.i18n.instructions.step3b">
+ <template #code="{ content }"
+ ><code>{{ content }}</code></template
+ >
+ </gl-sprintf>
+ </li>
+ <li class="gl-list-style-none">
+ <div class="gl-display-flex align-items-start">
+ <pre
+ :id="modalInfoCopyId"
+ class="gl-w-full"
+ data-testid="enable-review-app-copy-string"
+ >{{ modalInfoCopyStr }}</pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyToClipboardText"
+ :modal-id="modalId"
+ css-classes="border-0"
+ :target="`#${modalInfoCopyId}`"
+ />
+ </div>
+ </li>
+ </ul>
+ </li>
+ <li>{{ $options.i18n.instructions.step4 }}</li>
+ </ol>
+ <gl-link :href="$options.configuringReviewAppsPath" target="_blank">
+ {{ $options.i18n.learnMore }}
+ <gl-icon name="external-link" />
+ </gl-link>
+ <gl-link :href="$options.reviewAppsExamplesPath" target="_blank" class="gl-ml-6">
+ {{ $options.i18n.viewMoreExampleProjects }}
+ <gl-icon name="external-link" />
+ </gl-link>
</div>
- <p>
- <gl-sprintf :message="$options.instructionText.step3">
- <template #step="{ content }">
- <strong>{{ content }}</strong>
- </template>
- <template #link="{ content }">
- <gl-link :href="`blob/${defaultBranchName}/.gitlab-ci.yml`" target="_blank">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- <p>
- <gl-sprintf :message="$options.instructionText.step4">
- <template #step="{ content }">
- <strong>{{ content }}</strong>
- </template>
- <template #link="{ content }">
- <gl-link :href="$options.visualReviewsDocs" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index b8def676e7d..04a390fbba7 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,6 +1,8 @@
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { s__, __ } from '~/locale';
+import { isSafeURL } from '~/lib/utils/url_utility';
/**
* Renders the external url link in environments table.
@@ -8,6 +10,7 @@ import { s__ } from '~/locale';
export default {
components: {
GlButton,
+ ModalCopyButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -21,11 +24,19 @@ export default {
i18n: {
title: s__('Environments|Open live environment'),
open: s__('Environments|Open'),
+ copy: __('Copy URL'),
+ copyTitle: s__('Environments|Copy live environment URL'),
+ },
+ computed: {
+ isSafeUrl() {
+ return isSafeURL(this.externalUrl);
+ },
},
};
</script>
<template>
<gl-button
+ v-if="isSafeUrl"
v-gl-tooltip
:title="$options.i18n.title"
:aria-label="$options.i18n.title"
@@ -37,4 +48,7 @@ export default {
>
{{ $options.i18n.open }}
</gl-button>
+ <modal-copy-button v-else :title="$options.i18n.copyTitle" :text="externalUrl">
+ {{ $options.i18n.copy }}
+ </modal-copy-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_folder.vue b/app/assets/javascripts/environments/components/environment_folder.vue
index 881f404340d..2f6c54e4707 100644
--- a/app/assets/javascripts/environments/components/environment_folder.vue
+++ b/app/assets/javascripts/environments/components/environment_folder.vue
@@ -24,6 +24,10 @@ export default {
type: String,
required: true,
},
+ search: {
+ type: String,
+ required: true,
+ },
},
data() {
return { visible: false, interval: undefined };
@@ -32,7 +36,11 @@ export default {
folder: {
query: folderQuery,
variables() {
- return { environment: this.nestedEnvironment.latest, scope: this.scope };
+ return {
+ environment: this.nestedEnvironment.latest,
+ scope: this.scope,
+ search: this.search,
+ };
},
},
interval: {
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index f44182e822b..55e6a891e27 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,7 +1,9 @@
<script>
-import { GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
+import { GlBadge, GlPagination, GlSearchBoxByType, GlTab, GlTabs } from '@gitlab/ui';
+import { debounce } from 'lodash';
import { s__, __, sprintf } from '~/locale';
import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
import pageInfoQuery from '../graphql/queries/page_info.query.graphql';
@@ -31,6 +33,7 @@ export default {
StopEnvironmentModal,
GlBadge,
GlPagination,
+ GlSearchBoxByType,
GlTab,
GlTabs,
},
@@ -41,11 +44,10 @@ export default {
return {
scope: this.scope,
page: this.page ?? 1,
+ search: this.search,
};
},
- pollInterval() {
- return this.interval;
- },
+ pollInterval: 3000,
},
interval: {
query: pollIntervalQuery,
@@ -80,10 +82,11 @@ export default {
next: __('Next'),
prev: __('Prev'),
goto: (page) => sprintf(__('Go to page %{page}'), { page }),
+ searchPlaceholder: s__('Environments|Search by environment name'),
},
modalId: 'enable-review-app-info',
data() {
- const { page = '1', scope } = queryToObject(window.location.search);
+ const { page = '1', search = '', scope } = queryToObject(window.location.search);
return {
interval: undefined,
isReviewAppModalVisible: false,
@@ -97,6 +100,7 @@ export default {
environmentToStop: {},
environmentToChangeCanary: {},
weight: 0,
+ search,
};
},
computed: {
@@ -112,6 +116,9 @@ export default {
hasEnvironments() {
return this.environments.length > 0 || this.folders.length > 0;
},
+ hasSearch() {
+ return Boolean(this.search);
+ },
availableCount() {
return this.environmentApp?.availableCount;
},
@@ -152,11 +159,19 @@ export default {
return this.pageInfo?.perPage;
},
},
+ watch: {
+ interval(val) {
+ this.$apollo.queries.environmentApp.stopPolling();
+ this.$apollo.queries.environmentApp.startPolling(val);
+ },
+ },
mounted() {
window.addEventListener('popstate', this.syncPageFromQueryParams);
+ window.addEventListener('popstate', this.syncSearchFromQueryParams);
},
destroyed() {
window.removeEventListener('popstate', this.syncPageFromQueryParams);
+ window.removeEventListener('popstate', this.syncSearchFromQueryParams);
this.$apollo.queries.environmentApp.stopPolling();
},
methods: {
@@ -173,23 +188,24 @@ export default {
moveToPage(page) {
this.page = page;
updateHistory({
- url: setUrlParams({ page: this.page }),
+ url: setUrlParams({ page: this.page, scope: this.scope, search: this.search }),
title: document.title,
});
- this.resetPolling();
},
+ setSearch: debounce(function setSearch(input) {
+ this.search = input;
+ this.moveToPage(1);
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
syncPageFromQueryParams() {
const { page = '1' } = queryToObject(window.location.search);
this.page = parseInt(page, 10);
},
- resetPolling() {
- this.$apollo.queries.environmentApp.stopPolling();
+ syncSearchFromQueryParams() {
+ const { search = '' } = queryToObject(window.location.search);
+ this.search = search;
+ },
+ refetchEnvironments() {
this.$apollo.queries.environmentApp.refetch();
- this.$nextTick(() => {
- if (this.interval) {
- this.$apollo.queries.environmentApp.startPolling(this.interval);
- }
- });
},
},
ENVIRONMENTS_SCOPE,
@@ -237,12 +253,19 @@ export default {
</template>
</gl-tab>
</gl-tabs>
+ <gl-search-box-by-type
+ class="gl-mb-4"
+ :value="search"
+ :placeholder="$options.i18n.searchPlaceholder"
+ @input="setSearch"
+ />
<template v-if="hasEnvironments">
<environment-folder
v-for="folder in folders"
:key="folder.name"
class="gl-mb-3"
:scope="scope"
+ :search="search"
:nested-environment="folder"
/>
<environment-item
@@ -250,10 +273,15 @@ export default {
:key="environment.name"
class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid"
:environment="environment.latest"
- @change="resetPolling"
+ @change="refetchEnvironments"
/>
</template>
- <empty-state v-else :help-path="helpPagePath" :scope="scope" />
+ <empty-state
+ v-else-if="!$apollo.queries.environmentApp.loading"
+ :help-path="helpPagePath"
+ :scope="scope"
+ :has-term="hasSearch"
+ />
<gl-pagination
align="center"
:total-items="totalItems"
diff --git a/app/assets/javascripts/environments/components/environments_detail_header.vue b/app/assets/javascripts/environments/components/environments_detail_header.vue
index bd67908a6b4..bb2f053b3fc 100644
--- a/app/assets/javascripts/environments/components/environments_detail_header.vue
+++ b/app/assets/javascripts/environments/components/environments_detail_header.vue
@@ -4,6 +4,8 @@ import csrf from '~/lib/utils/csrf';
import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { isSafeURL } from '~/lib/utils/url_utility';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
@@ -16,6 +18,7 @@ export default {
TimeAgo,
DeleteEnvironmentModal,
StopEnvironmentModal,
+ ModalCopyButton,
},
directives: {
GlModalDirective,
@@ -73,6 +76,8 @@ export default {
deleteButtonText: s__('Environments|Delete'),
externalButtonTitle: s__('Environments|Open live environment'),
externalButtonText: __('View deployment'),
+ copyUrlText: __('Copy URL'),
+ copyUrlTitle: s__('Environments|Copy live environment URL'),
cancelAutoStopButtonTitle: __('Prevent environment from auto-stopping'),
},
computed: {
@@ -82,6 +87,9 @@ export default {
shouldShowExternalUrlButton() {
return Boolean(this.environment.externalUrl);
},
+ isSafeUrl() {
+ return isSafeURL(this.environment.externalUrl);
+ },
shouldShowStopButton() {
return this.canStopEnvironment && this.environment.isAvailable;
},
@@ -123,16 +131,25 @@ export default {
:href="terminalPath"
icon="terminal"
/>
- <gl-button
- v-if="shouldShowExternalUrlButton"
- v-gl-tooltip.hover
- data-testid="external-url-button"
- :title="$options.i18n.externalButtonTitle"
- :href="environment.externalUrl"
- icon="external-link"
- target="_blank"
- >{{ $options.i18n.externalButtonText }}</gl-button
- >
+ <template v-if="shouldShowExternalUrlButton">
+ <gl-button
+ v-if="isSafeUrl"
+ v-gl-tooltip.hover
+ data-testid="external-url-button"
+ :title="$options.i18n.externalButtonTitle"
+ :href="environment.externalUrl"
+ icon="external-link"
+ target="_blank"
+ >{{ $options.i18n.externalButtonText }}</gl-button
+ >
+ <modal-copy-button
+ v-else
+ :title="$options.i18n.copyUrlTitle"
+ :text="environment.externalUrl"
+ >
+ {{ $options.i18n.copyUrlText }}
+ </modal-copy-button>
+ </template>
<gl-button
v-if="shouldShowExternalUrlButton"
v-gl-tooltip.hover
diff --git a/app/assets/javascripts/environments/components/new_environment.vue b/app/assets/javascripts/environments/components/new_environment.vue
index 14da2668417..bb4d6ab3428 100644
--- a/app/assets/javascripts/environments/components/new_environment.vue
+++ b/app/assets/javascripts/environments/components/new_environment.vue
@@ -1,5 +1,5 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import EnvironmentForm from './environment_form.vue';
@@ -32,7 +32,7 @@ export default {
.then(({ data: { path } }) => visitUrl(path))
.catch((error) => {
const message = error.response.data.message[0];
- createFlash({ message });
+ createAlert({ message });
this.loading = false;
});
},
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 942491039d6..c4d02da9d21 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
// These statuses are based on how the backend defines pod phases here
// lib/gitlab/kubernetes/pod.rb
@@ -48,3 +48,32 @@ export const ENVIRONMENT_COUNT_BY_SCOPE = {
[ENVIRONMENTS_SCOPE.AVAILABLE]: 'availableCount',
[ENVIRONMENTS_SCOPE.STOPPED]: 'stoppedCount',
};
+
+export const REVIEW_APP_MODAL_I18N = {
+ title: s__('ReviewApp|Enable Review App'),
+ intro: s__(
+ 'EnableReviewApp|Review apps are dynamic environments that you can use to provide a live preview of changes made in a feature branch.',
+ ),
+ instructions: {
+ title: s__('EnableReviewApp|To configure a dynamic review app, you must:'),
+ step1: s__(
+ 'EnableReviewApp|Have access to infrastructure that can host and deploy the review apps.',
+ ),
+ step2: s__('EnableReviewApp|Install and configure a runner to do the deployment.'),
+ step3: s__('EnableReviewApp|Add a job in your CI/CD configuration that:'),
+ step3a: s__('EnableReviewApp|Only runs for feature branches or merge requests.'),
+ step3b: s__(
+ 'EnableReviewApp|Uses a predefined CI/CD variable like %{codeStart}$(CI_COMMIT_REF_SLUG)%{codeEnd} to dynamically create the review app environments. For example, for a configuration using merge request pipelines:',
+ ),
+ step4: s__('EnableReviewApp|Recommended: Set up a job that manually stops the Review Apps.'),
+ },
+ staticSitePopover: {
+ title: s__('EnableReviewApp|Using a static site?'),
+ body: s__(
+ 'EnableReviewApp|Make sure your project has an environment configured with the target URL set to your website URL. If not, create a new one before continuing.',
+ ),
+ },
+ learnMore: __('Learn more'),
+ viewMoreExampleProjects: s__('EnableReviewApp|View more example projects'),
+ copyToClipboardText: s__('EnableReviewApp|Copy snippet'),
+};
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
index c3ab9cf7fca..1a572208a1c 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
@@ -1,5 +1,5 @@
-query getEnvironmentApp($page: Int, $scope: String) {
- environmentApp(page: $page, scope: $scope) @client {
+query getEnvironmentApp($page: Int, $scope: String, $search: String) {
+ environmentApp(page: $page, scope: $scope, search: $search) @client {
availableCount
stoppedCount
environments
diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
index e8c145ee916..c662acb8f93 100644
--- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
@@ -1,5 +1,5 @@
-query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String) {
- folder(environment: $environment, scope: $scope) @client {
+query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String, $search: String) {
+ folder(environment: $environment, scope: $scope, search: $search) @client {
availableCount
environments
stoppedCount
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index 722bb78bcf9..afd56d0cf0d 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -30,8 +30,8 @@ const mapEnvironment = (env) => ({
export const resolvers = (endpoint) => ({
Query: {
- environmentApp(_context, { page, scope }, { cache }) {
- return axios.get(endpoint, { params: { nested: true, page, scope } }).then((res) => {
+ environmentApp(_context, { page, scope, search }, { cache }) {
+ return axios.get(endpoint, { params: { nested: true, page, scope, search } }).then((res) => {
const headers = normalizeHeaders(res.headers);
const interval = headers['POLL-INTERVAL'];
const pageInfo = { ...parseIntPagination(headers), __typename: 'LocalPageInfo' };
@@ -59,8 +59,8 @@ export const resolvers = (endpoint) => ({
};
});
},
- folder(_, { environment: { folderPath }, scope }) {
- return axios.get(folderPath, { params: { scope, per_page: 3 } }).then((res) => ({
+ folder(_, { environment: { folderPath }, scope, search }) {
+ return axios.get(folderPath, { params: { scope, search, per_page: 3 } }).then((res) => ({
availableCount: res.data.available_count,
environments: res.data.environments.map(mapEnvironment),
stoppedCount: res.data.stopped_count,
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 8957a3074ed..5e936ad8c96 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -3,7 +3,7 @@
*/
import { isEqual, isFunction, omitBy } from 'lodash';
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Poll from '~/lib/utils/poll';
import { getParameterByName } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
@@ -94,7 +94,7 @@ export default {
errorCallback() {
this.isLoading = false;
- createFlash({
+ createAlert({
message: s__('Environments|An error occurred while fetching the environments.'),
});
},
@@ -123,7 +123,7 @@ export default {
})
.catch((err) => {
this.isLoading = false;
- createFlash({
+ createAlert({
message: isFunction(errorMessage) ? errorMessage(err.response.data) : errorMessage,
});
});
@@ -179,7 +179,7 @@ export default {
window.location.href = url.join('/');
})
.catch(() => {
- createFlash({
+ createAlert({
message: errorMessage,
});
});
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index a602c92a840..b02c3cd2cba 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -13,7 +13,7 @@ import {
GlIcon,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/flash';
import { __, sprintf, n__ } from '~/locale';
import Tracking from '~/tracking';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -89,7 +89,7 @@ export default {
pollInterval: 2000,
update: (data) => data.project.sentryErrors.detailedError,
error: () =>
- createFlash({
+ createAlert({
message: __('Failed to load error details from Sentry.'),
}),
result(res) {
@@ -234,9 +234,9 @@ export default {
if (Date.now() > this.errorPollTimeout) {
this.$apollo.queries.error.stopPolling();
this.errorLoading = false;
- createFlash({
+ createAlert({
message: __('Could not connect to Sentry. Refresh the page to try again.'),
- type: 'warning',
+ variant: VARIANT_WARNING,
});
}
},
diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js
index fbfcd6ce2df..603f8611005 100644
--- a/app/assets/javascripts/error_tracking/store/actions.js
+++ b/app/assets/javascripts/error_tracking/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import service from '../services';
@@ -18,7 +18,7 @@ export const updateStatus = ({ commit }, { endpoint, redirectUrl, status }) =>
return resp.data.result;
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Failed to update issue status'),
}),
);
diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js
index 09fa650f64b..1409399940a 100644
--- a/app/assets/javascripts/error_tracking/store/details/actions.js
+++ b/app/assets/javascripts/error_tracking/store/details/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import service from '../../services';
@@ -26,7 +26,7 @@ export function startPollingStacktrace({ commit }, endpoint) {
},
errorCallback: () => {
commit(types.SET_LOADING_STACKTRACE, false);
- createFlash({
+ createAlert({
message: __('Failed to load stacktrace.'),
});
},
diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index 418056314f6..f633711add3 100644
--- a/app/assets/javascripts/error_tracking/store/list/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import Service from '../../services';
@@ -33,7 +33,7 @@ export function startPolling({ state, commit, dispatch }) {
},
errorCallback: () => {
commit(types.SET_LOADING, false);
- createFlash({
+ createAlert({
message: __('Failed to load errors from Sentry.'),
});
},
diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue
index 70fb1fa9cd7..3bc91a2adbf 100644
--- a/app/assets/javascripts/error_tracking_settings/components/app.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/app.vue
@@ -190,7 +190,7 @@ export default {
<gl-form-radio name="error-tracking-integrated" :value="true">
{{ __('GitLab') }}
<template #help>
- {{ __('Uses GitLab as a lightweight alternative to Sentry.') }}
+ {{ __('Uses GitLab as an alternative to Sentry.') }}
</template>
</gl-form-radio>
</gl-form-radio-group>
diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js
index 972ad58c617..4d6fe767f3a 100644
--- a/app/assets/javascripts/error_tracking_settings/store/actions.js
+++ b/app/assets/javascripts/error_tracking_settings/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -46,7 +46,7 @@ export const requestSettings = ({ commit }) => {
export const receiveSettingsError = ({ commit }, { response = {} }) => {
const message = response.data && response.data.message ? response.data.message : '';
- createFlash({
+ createAlert({
message: `${__('There was an error saving your changes.')} ${message}`,
});
commit(types.UPDATE_SETTINGS_LOADING, false);
diff --git a/app/assets/javascripts/feature_flags/components/environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/environments_dropdown.vue
index 70b60b4b113..ce5f7915dbf 100644
--- a/app/assets/javascripts/feature_flags/components/environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/environments_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlSearchBoxByType } from '@gitlab/ui';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -87,7 +87,7 @@ export default {
.catch(() => {
this.isLoading = false;
this.closeSuggestions();
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again.'),
});
});
diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
index 98982920121..89400bc4742 100644
--- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
@@ -8,7 +8,7 @@ import {
GlSearchBoxByType,
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
@@ -52,7 +52,7 @@ export default {
this.results = data || [];
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again.'),
});
})
diff --git a/app/assets/javascripts/feature_flags/store/edit/actions.js b/app/assets/javascripts/feature_flags/store/edit/actions.js
index 8656479190a..97c22781ac5 100644
--- a/app/assets/javascripts/feature_flags/store/edit/actions.js
+++ b/app/assets/javascripts/feature_flags/store/edit/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -49,7 +49,7 @@ export const receiveFeatureFlagSuccess = ({ commit }, response) =>
commit(types.RECEIVE_FEATURE_FLAG_SUCCESS, response);
export const receiveFeatureFlagError = ({ commit }) => {
commit(types.RECEIVE_FEATURE_FLAG_ERROR);
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again!'),
});
};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
index b26a96499ba..a9542a9667e 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -10,7 +10,7 @@ export function dismiss(endpoint, highlightId) {
feature_name: highlightId,
})
.catch(() =>
- createFlash({
+ createAlert({
message: __(
'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.',
),
diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
index 9726b2164b7..23591fc0667 100644
--- a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
+++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import AjaxFilter from './droplab/plugins/ajax_filter';
import DropdownUtils from './dropdown_utils';
@@ -27,7 +27,7 @@ export default class DropdownAjaxFilter extends FilteredSearchDropdown {
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
onError() {
- createFlash({
+ createAlert({
message: __('An error occurred fetching the dropdown data.'),
});
},
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index aeea66bf51c..8c50c1860ec 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import Ajax from './droplab/plugins/ajax';
import Filter from './droplab/plugins/filter';
@@ -14,7 +14,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
method: 'setData',
loadingTemplate: this.loadingTemplate,
onError() {
- createFlash({
+ createAlert({
message: __('An error occurred fetching the dropdown data.'),
});
},
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index ddc3c06a9d1..ab95986dc62 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import Ajax from './droplab/plugins/ajax';
import Filter from './droplab/plugins/filter';
@@ -17,7 +17,7 @@ export default class DropdownNonUser extends FilteredSearchDropdown {
loadingTemplate: this.loadingTemplate,
preprocessing,
onError() {
- createFlash({
+ createAlert({
message: __('An error occurred fetching the dropdown data.'),
});
},
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index ac2cf27e873..bc0f5398b4c 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,7 +1,7 @@
import { last } from 'lodash';
import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import {
ENTER_KEY_CODE,
BACKSPACE_KEY_CODE,
@@ -91,7 +91,7 @@ export default class FilteredSearchManager {
.fetch()
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
- createFlash({
+ createAlert({
message: __('An error occurred while parsing recent searches'),
});
// Gracefully fail to empty array
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index 0d144398531..1ad2006d689 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -4,7 +4,7 @@ import * as Emoji from '~/emoji';
import FilteredSearchContainer from '~/filtered_search/container';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale';
@@ -85,7 +85,7 @@ export default class VisualTokenValue {
);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while fetching label colors.'),
}),
);
@@ -111,7 +111,7 @@ export default class VisualTokenValue {
VisualTokenValue.replaceEpicTitle(tokenValueContainer, matchingEpic.title, matchingEpic.id);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while adding formatted title for epic'),
}),
);
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index edf83a33812..5665231e613 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -158,7 +158,7 @@ const createAlert = function createAlert({
onDismiss();
}
this.$destroy();
- this.$el.parentNode.removeChild(this.$el);
+ this.$el.parentNode?.removeChild(this.$el);
},
},
render(h) {
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index d376c9f76ba..0a4733de65f 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
@@ -19,7 +19,7 @@ export default class GpgBadges {
badges.children().attr('aria-label', __('Loading'));
const displayError = () =>
- createFlash({
+ createAlert({
message: __('An error occurred while loading commit signatures'),
});
diff --git a/app/assets/javascripts/grafana_integration/store/actions.js b/app/assets/javascripts/grafana_integration/store/actions.js
index 25347ad6433..db2fd3cc256 100644
--- a/app/assets/javascripts/grafana_integration/store/actions.js
+++ b/app/assets/javascripts/grafana_integration/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -38,7 +38,7 @@ export const receiveGrafanaIntegrationUpdateError = (_, error) => {
const { response } = error;
const message = response.data && response.data.message ? response.data.message : '';
- createFlash({
+ createAlert({
message: `${__('There was an error saving your changes.')} ${message}`,
});
};
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index e86103c332b..3b737dfff33 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -4,14 +4,13 @@ import { concatPagination } from '@apollo/client/utilities';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import createDefaultClient from '~/lib/graphql';
import typeDefs from '~/work_items/graphql/typedefs.graphql';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-import { WIDGET_TYPE_LABELS } from '~/work_items/constants';
+import { WIDGET_TYPE_MILESTONE } from '~/work_items/constants';
export const temporaryConfig = {
typeDefs,
cacheConfig: {
possibleTypes: {
- LocalWorkItemWidget: ['LocalWorkItemLabels'],
+ LocalWorkItemWidget: ['LocalWorkItemMilestone'],
},
typePolicies: {
Project: {
@@ -28,18 +27,32 @@ export const temporaryConfig = {
return (
widgets || [
{
- __typename: 'LocalWorkItemLabels',
- type: WIDGET_TYPE_LABELS,
- allowScopedLabels: true,
- nodes: [],
+ __typename: 'LocalWorkItemMilestone',
+ type: WIDGET_TYPE_MILESTONE,
+ nodes: [
+ {
+ dueDate: null,
+ expired: false,
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Milestone',
+ },
+ ],
},
]
);
},
},
widgets: {
- merge(_, incoming) {
- return incoming;
+ merge(existing = [], incoming) {
+ if (existing.length === 0) {
+ return incoming;
+ }
+ return existing.map((existingWidget) => {
+ const incomingWidget = incoming.find((w) => w.type === existingWidget.type);
+ return incomingWidget || existingWidget;
+ });
},
},
},
@@ -62,27 +75,6 @@ export const resolvers = {
});
cache.writeQuery({ query: getIssueStateQuery, data });
},
- localUpdateWorkItem(_, { input }, { cache }) {
- const sourceData = cache.readQuery({
- query: workItemQuery,
- variables: { id: input.id },
- });
-
- const data = produce(sourceData, (draftData) => {
- if (input.labels) {
- const labelsWidget = draftData.workItem.mockWidgets.find(
- (widget) => widget.type === WIDGET_TYPE_LABELS,
- );
- labelsWidget.nodes = [...input.labels];
- }
- });
-
- cache.writeQuery({
- query: workItemQuery,
- variables: { id: input.id },
- data,
- });
- },
},
};
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index c6bd9e563c0..545c150e536 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -144,7 +144,7 @@
"WorkItemWidgetIteration",
"WorkItemWidgetLabels",
"WorkItemWidgetStartAndDueDate",
- "WorkItemWidgetVerificationStatus",
+ "WorkItemWidgetStatus",
"WorkItemWidgetWeight"
]
}
diff --git a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
index f64c4276deb..c05b4a5950c 100644
--- a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
@@ -9,6 +9,7 @@ query projectUsersSearch($search: String!, $fullPath: ID!, $after: String, $firs
relations: [DIRECT, INHERITED, INVITED_GROUPS]
first: $first
after: $after
+ sort: USER_FULL_NAME_ASC
) {
pageInfo {
hasNextPage
diff --git a/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql b/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql
index 2bd016feb19..5a589b094de 100644
--- a/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql
@@ -8,7 +8,11 @@ query projectUsersSearchWithMRPermissions(
) {
workspace: project(fullPath: $fullPath) {
id
- users: projectMembers(search: $search, relations: [DIRECT, INHERITED, INVITED_GROUPS]) {
+ users: projectMembers(
+ search: $search
+ relations: [DIRECT, INHERITED, INVITED_GROUPS]
+ sort: USER_FULL_NAME_ASC
+ ) {
nodes {
id
mergeRequestInteraction(id: $mergeRequestId) {
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index 49e7dd28ff6..cc70d832edc 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,6 +1,6 @@
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { getGroupPathAvailability } from '~/rest_api';
import axios from '~/lib/utils/axios_utils';
@@ -77,7 +77,7 @@ export default class Group {
element.value = suggestedSlug;
});
} else if (exists && !suggests.length) {
- createFlash({
+ createAlert({
message: __('Unable to suggest a path. Please refresh and try again.'),
});
}
@@ -87,7 +87,7 @@ export default class Group {
return;
}
- createFlash({
+ createAlert({
message: __('An error occurred while checking group path. Please refresh and try again.'),
});
});
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 0bd7371d39b..15f5a3518a5 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { mergeUrlParams, getParameterByName } from '~/lib/utils/url_utility';
import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
@@ -51,7 +51,6 @@ export default {
isModalVisible: false,
isLoading: true,
isSearchEmpty: false,
- searchEmptyMessage: '',
targetGroup: null,
targetParentGroup: null,
showEmptyState: false,
@@ -88,15 +87,12 @@ export default {
},
},
created() {
- this.searchEmptyMessage = this.hideProjects
- ? COMMON_STR.GROUP_SEARCH_EMPTY
- : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
-
eventHub.$on(`${this.action}fetchPage`, this.fetchPage);
eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren);
eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
eventHub.$on(`${this.action}updatePagination`, this.updatePagination);
eventHub.$on(`${this.action}updateGroups`, this.updateGroups);
+ eventHub.$on(`${this.action}fetchFilteredAndSortedGroups`, this.fetchFilteredAndSortedGroups);
},
mounted() {
this.fetchAllGroups();
@@ -111,6 +107,7 @@ export default {
eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
eventHub.$off(`${this.action}updatePagination`, this.updatePagination);
eventHub.$off(`${this.action}updateGroups`, this.updateGroups);
+ eventHub.$off(`${this.action}fetchFilteredAndSortedGroups`, this.fetchFilteredAndSortedGroups);
},
methods: {
hideModal() {
@@ -132,7 +129,7 @@ export default {
this.isLoading = false;
window.scrollTo({ top: 0, behavior: 'smooth' });
- createFlash({ message: COMMON_STR.FAILURE });
+ createAlert({ message: COMMON_STR.FAILURE });
});
},
fetchAllGroups() {
@@ -153,6 +150,18 @@ export default {
this.updateGroups(res, Boolean(this.filterGroupsBy));
});
},
+ fetchFilteredAndSortedGroups({ filterGroupsBy, sortBy }) {
+ this.isLoading = true;
+
+ return this.fetchGroups({
+ filterGroupsBy,
+ sortBy,
+ updatePagination: true,
+ }).then((res) => {
+ this.isLoading = false;
+ this.updateGroups(res, Boolean(filterGroupsBy));
+ });
+ },
fetchPage({ page, filterGroupsBy, sortBy, archived }) {
this.isLoading = true;
@@ -218,7 +227,7 @@ export default {
if (err.status === 403) {
message = COMMON_STR.LEAVE_FORBIDDEN;
}
- createFlash({ message });
+ createAlert({ message });
this.targetGroup.isBeingRemoved = false;
});
},
@@ -245,7 +254,7 @@ export default {
const hasGroups = groups && groups.length > 0;
if (this.renderEmptyState) {
- this.isSearchEmpty = this.filterGroupsBy !== null && !hasGroups;
+ this.isSearchEmpty = fromSearch && !hasGroups;
} else {
this.isSearchEmpty = !hasGroups;
}
@@ -280,7 +289,6 @@ export default {
v-else
:groups="groups"
:search-empty="isSearchEmpty"
- :search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
:action="action"
/>
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 3a05c308a2a..43aa0753082 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -1,11 +1,18 @@
<script>
+import { GlEmptyState } from '@gitlab/ui';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { getParameterByName } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import eventHub from '../event_hub';
export default {
+ i18n: {
+ emptyStateTitle: __('No results found'),
+ emptyStateDescription: __('Edit your search and try again'),
+ },
components: {
PaginationLinks,
+ GlEmptyState,
},
props: {
groups: {
@@ -20,10 +27,6 @@ export default {
type: Boolean,
required: true,
},
- searchEmptyMessage: {
- type: String,
- required: true,
- },
action: {
type: String,
required: false,
@@ -43,12 +46,11 @@ export default {
<template>
<div class="groups-list-tree-container" data-qa-selector="groups_list_tree_container">
- <div
+ <gl-empty-state
v-if="searchEmpty"
- class="has-no-search-results gl-font-style-italic gl-text-center gl-text-gray-600 gl-p-5"
- >
- {{ searchEmptyMessage }}
- </div>
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ />
<template v-else>
<group-folder :groups="groups" :action="action" />
<pagination-links
diff --git a/app/assets/javascripts/groups/components/new_top_level_group_alert.vue b/app/assets/javascripts/groups/components/new_top_level_group_alert.vue
new file mode 100644
index 00000000000..c6af6cdb59f
--- /dev/null
+++ b/app/assets/javascripts/groups/components/new_top_level_group_alert.vue
@@ -0,0 +1,40 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export default {
+ name: 'NewTopLevelGroupAlert',
+ components: {
+ GlAlert,
+ UserCalloutDismisser,
+ },
+ i18n: {
+ titleText: s__("Groups|You're creating a new top-level group"),
+ bodyText: s__(
+ 'Groups|Members, projects, trials, and paid subscriptions are tied to a specific top-level group. If you are already a member of a top-level group, you can create a subgroup so your new work is part of your existing top-level group. Do you want to create a subgroup instead?',
+ ),
+ primaryBtnText: s__('Groups|Learn more about subgroups'),
+ },
+ subgroupsDocsPath: helpPagePath('user/group/subgroups/index'),
+};
+</script>
+
+<template>
+ <user-callout-dismisser feature-name="new_top_level_group_alert">
+ <template #default="{ dismiss, shouldShowCallout }">
+ <gl-alert
+ v-if="shouldShowCallout"
+ ref="newTopLevelAlert"
+ data-testid="new-top-level-alert"
+ :title="$options.i18n.titleText"
+ :primary-button-text="$options.i18n.primaryBtnText"
+ :primary-button-link="$options.subgroupsDocsPath"
+ @dismiss="dismiss"
+ >
+ {{ $options.i18n.bodyText }}
+ </gl-alert>
+ </template>
+ </user-callout-dismisser>
+</template>
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
index 325e42af0f8..d0c5846ac88 100644
--- a/app/assets/javascripts/groups/components/overview_tabs.vue
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -1,58 +1,77 @@
<script>
-import { GlTabs, GlTab } from '@gitlab/ui';
-import { isString } from 'lodash';
+import { GlTabs, GlTab, GlSearchBoxByType, GlSorting, GlSortingItem } from '@gitlab/ui';
+import { isString, debounce } from 'lodash';
import { __ } from '~/locale';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import GroupsStore from '../store/groups_store';
import GroupsService from '../service/groups_service';
import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
ACTIVE_TAB_SHARED,
ACTIVE_TAB_ARCHIVED,
+ OVERVIEW_TABS_SORTING_ITEMS,
} from '../constants';
+import eventHub from '../event_hub';
import GroupsApp from './app.vue';
+const [SORTING_ITEM_NAME] = OVERVIEW_TABS_SORTING_ITEMS;
+
export default {
- components: { GlTabs, GlTab, GroupsApp },
- inject: ['endpoints'],
+ components: { GlTabs, GlTab, GroupsApp, GlSearchBoxByType, GlSorting, GlSortingItem },
+ inject: ['endpoints', 'initialSort'],
data() {
+ const tabs = [
+ {
+ title: this.$options.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
+ key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ renderEmptyState: true,
+ lazy: this.$route.name !== ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
+ store: new GroupsStore({ showSchemaMarkup: true }),
+ },
+ {
+ title: this.$options.i18n[ACTIVE_TAB_SHARED],
+ key: ACTIVE_TAB_SHARED,
+ renderEmptyState: false,
+ lazy: this.$route.name !== ACTIVE_TAB_SHARED,
+ service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
+ store: new GroupsStore(),
+ },
+ {
+ title: this.$options.i18n[ACTIVE_TAB_ARCHIVED],
+ key: ACTIVE_TAB_ARCHIVED,
+ renderEmptyState: false,
+ lazy: this.$route.name !== ACTIVE_TAB_ARCHIVED,
+ service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
+ store: new GroupsStore(),
+ },
+ ];
return {
- tabs: [
- {
- title: this.$options.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
- key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
- renderEmptyState: true,
- lazy: false,
- service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
- store: new GroupsStore({ showSchemaMarkup: true }),
- },
- {
- title: this.$options.i18n[ACTIVE_TAB_SHARED],
- key: ACTIVE_TAB_SHARED,
- renderEmptyState: false,
- lazy: true,
- service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
- store: new GroupsStore(),
- },
- {
- title: this.$options.i18n[ACTIVE_TAB_ARCHIVED],
- key: ACTIVE_TAB_ARCHIVED,
- renderEmptyState: false,
- lazy: true,
- service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
- store: new GroupsStore(),
- },
- ],
- activeTabIndex: 0,
+ tabs,
+ activeTabIndex: tabs.findIndex((tab) => tab.key === this.$route.name),
+ sort: SORTING_ITEM_NAME,
+ isAscending: true,
+ search: '',
};
},
+ computed: {
+ activeTab() {
+ return this.tabs[this.activeTabIndex];
+ },
+ sortQueryStringValue() {
+ return this.isAscending ? this.sort.asc : this.sort.desc;
+ },
+ },
mounted() {
- const activeTabIndex = this.tabs.findIndex((tab) => tab.key === this.$route.name);
-
- if (activeTabIndex === -1) {
- return;
- }
+ this.search = this.$route.query?.filter || '';
- this.activeTabIndex = activeTabIndex;
+ const sortQueryStringValue = this.$route.query?.sort || this.initialSort;
+ const sort =
+ OVERVIEW_TABS_SORTING_ITEMS.find((sortOption) =>
+ [sortOption.asc, sortOption.desc].includes(sortQueryStringValue),
+ ) || SORTING_ITEM_NAME;
+ this.sort = sort;
+ this.isAscending = sort.asc === sortQueryStringValue;
},
methods: {
handleTabInput(tabIndex) {
@@ -72,14 +91,64 @@ export default {
? this.$route.params.group.split('/')
: this.$route.params.group;
- this.$router.push({ name: tab.key, params: { group: groupParam } });
+ this.$router.push({ name: tab.key, params: { group: groupParam }, query: this.$route.query });
+ },
+ handleSearchOrSortChange() {
+ // Update query string
+ const query = {};
+ if (this.sortQueryStringValue !== this.initialSort) {
+ query.sort = this.isAscending ? this.sort.asc : this.sort.desc;
+ }
+ if (this.search) {
+ query.filter = this.search;
+ }
+ this.$router.push({ query });
+
+ // Reset `lazy` prop so that groups/projects are fetched with updated `sort` and `filter` params when switching tabs
+ this.tabs.forEach((tab, index) => {
+ if (index === this.activeTabIndex) {
+ return;
+ }
+ // eslint-disable-next-line no-param-reassign
+ tab.lazy = true;
+ });
+
+ // Update data
+ eventHub.$emit(`${this.activeTab.key}fetchFilteredAndSortedGroups`, {
+ filterGroupsBy: this.search,
+ sortBy: this.sortQueryStringValue,
+ });
+ },
+ handleSortDirectionChange() {
+ this.isAscending = !this.isAscending;
+
+ this.handleSearchOrSortChange();
+ },
+ handleSortingItemClick(sortingItem) {
+ if (sortingItem === this.sort) {
+ return;
+ }
+
+ this.sort = sortingItem;
+
+ this.handleSearchOrSortChange();
+ },
+ handleSearchInput(value) {
+ this.search = value;
+
+ this.debouncedSearch();
},
+ debouncedSearch: debounce(async function debouncedSearch() {
+ this.handleSearchOrSortChange();
+ }, DEBOUNCE_DELAY),
},
i18n: {
[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: __('Subgroups and projects'),
[ACTIVE_TAB_SHARED]: __('Shared projects'),
[ACTIVE_TAB_ARCHIVED]: __('Archived projects'),
+ searchPlaceholder: __('Search'),
},
+ OVERVIEW_TABS_SORTING_ITEMS,
};
</script>
@@ -99,5 +168,37 @@ export default {
:render-empty-state="renderEmptyState"
/>
</gl-tab>
+ <template #tabs-end>
+ <li class="gl-flex-grow-1 gl-align-self-center gl-w-full gl-lg-w-auto gl-py-2">
+ <div class="gl-lg-display-flex gl-justify-content-end gl-mx-n2 gl-my-n2">
+ <div class="gl-p-2 gl-lg-form-input-md gl-w-full">
+ <gl-search-box-by-type
+ :value="search"
+ :placeholder="$options.i18n.searchPlaceholder"
+ data-qa-selector="groups_filter_field"
+ @input="handleSearchInput"
+ />
+ </div>
+ <div class="gl-p-2 gl-w-full gl-lg-w-auto">
+ <gl-sorting
+ class="gl-w-full"
+ dropdown-class="gl-w-full"
+ data-testid="group_sort_by_dropdown"
+ :text="sort.label"
+ :is-ascending="isAscending"
+ @sortDirectionChange="handleSortDirectionChange"
+ >
+ <gl-sorting-item
+ v-for="sortingItem in $options.OVERVIEW_TABS_SORTING_ITEMS"
+ :key="sortingItem.label"
+ :active="sortingItem === sort"
+ @click="handleSortingItemClick(sortingItem)"
+ >{{ sortingItem.label }}</gl-sorting-item
+ >
+ </gl-sorting>
+ </div>
+ </div>
+ </li>
+ </template>
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/groups/components/transfer_group_form.vue b/app/assets/javascripts/groups/components/transfer_group_form.vue
index 7e7282a27b0..e28459811d7 100644
--- a/app/assets/javascripts/groups/components/transfer_group_form.vue
+++ b/app/assets/javascripts/groups/components/transfer_group_form.vue
@@ -2,7 +2,7 @@
import { GlFormGroup } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
+import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
export const i18n = {
confirmationMessage: __(
diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js
index 223c2975c11..6fb12cd6270 100644
--- a/app/assets/javascripts/groups/constants.js
+++ b/app/assets/javascripts/groups/constants.js
@@ -24,8 +24,6 @@ export const COMMON_STR = {
EDIT_BTN_TITLE: s__('GroupsTree|Edit'),
REMOVE_BTN_TITLE: s__('GroupsTree|Delete'),
OPTIONS_DROPDOWN_TITLE: s__('GroupsTree|Options'),
- GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'),
- GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'),
};
export const ITEM_TYPE = {
@@ -62,3 +60,26 @@ export const VISIBILITY_TYPE_ICON = {
[VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield',
[VISIBILITY_LEVEL_PRIVATE_STRING]: 'lock',
};
+
+export const OVERVIEW_TABS_SORTING_ITEMS = [
+ {
+ label: __('Name'),
+ asc: 'name_asc',
+ desc: 'name_desc',
+ },
+ {
+ label: __('Created'),
+ asc: 'created_asc',
+ desc: 'created_desc',
+ },
+ {
+ label: __('Updated'),
+ asc: 'latest_activity_asc',
+ desc: 'latest_activity_desc',
+ },
+ {
+ label: __('Stars'),
+ asc: 'stars_asc',
+ desc: 'stars_desc',
+ },
+];
diff --git a/app/assets/javascripts/groups/init_overview_tabs.js b/app/assets/javascripts/groups/init_overview_tabs.js
index 4fa3682c729..664d07ca13d 100644
--- a/app/assets/javascripts/groups/init_overview_tabs.js
+++ b/app/assets/javascripts/groups/init_overview_tabs.js
@@ -51,6 +51,7 @@ export const initGroupOverviewTabs = () => {
subgroupsAndProjectsEndpoint,
sharedProjectsEndpoint,
archivedProjectsEndpoint,
+ initialSort,
} = el.dataset;
return new Vue({
@@ -70,6 +71,7 @@ export const initGroupOverviewTabs = () => {
[ACTIVE_TAB_SHARED]: sharedProjectsEndpoint,
[ACTIVE_TAB_ARCHIVED]: archivedProjectsEndpoint,
},
+ initialSort,
},
render(createElement) {
return createElement(OverviewTabs);
diff --git a/app/assets/javascripts/groups/settings/components/access_dropdown.vue b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
index 28f059fa23e..db8e424e166 100644
--- a/app/assets/javascripts/groups/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__, n__ } from '~/locale';
import { getSubGroups } from '../api/access_dropdown_api';
import { LEVEL_TYPES } from '../constants';
@@ -98,7 +98,7 @@ export default {
this.consolidateData(groupsResponse.data);
this.setSelected({ initial });
})
- .catch(() => createFlash({ message: __('Failed to load groups.') }))
+ .catch(() => createAlert({ message: __('Failed to load groups.') }))
.finally(() => {
this.initialLoading = false;
this.loading = false;
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 64bba91eb4d..34e984a9bb9 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,21 +1,11 @@
import $ from 'jquery';
import { escape } from 'lodash';
+import { groupsPath } from '~/vue_shared/components/group_select/utils';
import { __ } from '~/locale';
import Api from './api';
import { loadCSSFile } from './lib/utils/css_utils';
import { select2AxiosTransport } from './lib/utils/select2_utils';
-const groupsPath = (groupsFilter, parentGroupID) => {
- switch (groupsFilter) {
- case 'descendant_groups':
- return Api.descendantGroupsPath.replace(':id', parentGroupID);
- case 'subgroups':
- return Api.subgroupsPath.replace(':id', parentGroupID);
- default:
- return Api.groupsPath;
- }
-};
-
const groupsSelect = () => {
loadCSSFile(gon.select2_css_path)
.then(() => {
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index f4b939fb20f..8fc0ce48e61 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -14,6 +14,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { truncate } from '~/lib/utils/text_utility';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__, sprintf } from '~/locale';
+import Tracking from '~/tracking';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import {
FIRST_DROPDOWN_INDEX,
@@ -163,8 +164,17 @@ export default {
...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
openDropdown() {
this.showDropdown = true;
- this.isFocused = true;
- this.$emit('expandSearchBar', true);
+
+ // check isFocused state to avoid firing duplicate events
+ if (!this.isFocused) {
+ this.isFocused = true;
+ this.$emit('expandSearchBar', true);
+
+ Tracking.event(undefined, 'focus_input', {
+ label: 'global_search',
+ property: 'top_navigation',
+ });
+ }
},
closeDropdown() {
this.showDropdown = false;
@@ -178,6 +188,11 @@ export default {
this.showDropdown = false;
this.isFocused = false;
this.$emit('collapseSearchBar');
+
+ Tracking.event(undefined, 'blur_input', {
+ label: 'global_search',
+ property: 'top_navigation',
+ });
}, 200);
},
submitSearch() {
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
index c184e25f67f..00059d01308 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/description.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -23,7 +23,12 @@ export default {
<template>
<div class="d-flex align-items-center">
- <ci-icon is-borderless :status="job.status" :size="24" class="d-flex" />
+ <ci-icon
+ is-borderless
+ :status="job.status"
+ :size="24"
+ class="gl-align-items-center gl-border gl-display-inline-flex gl-z-index-1"
+ />
<span class="gl-ml-3">
{{ job.name }}
<a
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 2284ffb8480..4d8c62d3430 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlIcon, GlTooltipDirective, GlBadge } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import { __ } from '~/locale';
import Item from './item.vue';
export default {
@@ -10,7 +10,6 @@ export default {
components: {
GlIcon,
GlBadge,
- CiIcon,
Item,
GlLoadingIcon,
},
@@ -27,11 +26,15 @@ export default {
},
computed: {
collapseIcon() {
- return this.stage.isCollapsed ? 'chevron-lg-left' : 'chevron-lg-down';
+ return this.stage.isCollapsed ? 'chevron-lg-down' : 'chevron-lg-up';
},
showLoadingIcon() {
return this.stage.isLoading && !this.stage.jobs.length;
},
+ stageTitle() {
+ const prefix = __('Stage');
+ return `${prefix}: ${this.stage.name}`;
+ },
jobsCount() {
return this.stage.jobs.length;
},
@@ -57,29 +60,29 @@ export default {
<template>
<div class="ide-stage card gl-mt-3">
<div
- ref="cardHeader"
:class="{
'border-bottom-0': stage.isCollapsed,
}"
- class="card-header"
+ class="card-header gl-align-items-center gl-cursor-pointer gl-display-flex"
+ data-testid="card-header"
@click="toggleCollapsed"
>
- <ci-icon :status="stage.status" :size="24" />
<strong
ref="stageTitle"
v-gl-tooltip="showTooltip"
:title="showTooltip ? stage.name : null"
data-container="body"
- class="gl-ml-3 text-truncate"
+ class="gl-text-truncate"
+ data-testid="stage-title"
>
- {{ stage.name }}
+ {{ stageTitle }}
</strong>
<div v-if="!stage.isLoading || stage.jobs.length" class="gl-mr-3 gl-ml-2">
<gl-badge>{{ jobsCount }}</gl-badge>
</div>
- <gl-icon :name="collapseIcon" class="ide-stage-collapse-icon" />
+ <gl-icon :name="collapseIcon" class="gl-absolute gl-right-5" />
</div>
- <div v-show="!stage.isCollapsed" ref="jobList" class="card-body p-0">
+ <div v-show="!stage.isCollapsed" class="card-body p-0" data-testid="job-list">
<gl-loading-icon v-if="showLoadingIcon" size="sm" />
<template v-else>
<item v-for="job in stage.jobs" :key="job.id" :job="job" @clickViewLog="clickViewLog" />
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 9684bf8f18c..dbfaeba9708 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlModal, GlButton } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import { modalTypes } from '../../constants';
import { trimPathComponents, getPathParent } from '../../utils';
@@ -77,7 +77,7 @@ export default {
if (this.modalType === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
- createFlash({
+ createAlert({
message: sprintf(__('The name "%{name}" is already taken in this directory.'), {
name: this.entryName,
}),
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index a1396995a3b..5f35dbdc5e7 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -11,7 +11,7 @@ import {
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
import SourceEditor from '~/editor/source_editor';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import ModelManager from '~/ide/lib/common/model_manager';
import { defaultDiffEditorOptions, defaultEditorOptions } from '~/ide/lib/editor_options';
import { __ } from '~/locale';
@@ -239,7 +239,7 @@ export default {
this.createEditorInstance();
})
.catch((err) => {
- createFlash({
+ createAlert({
message: __('Error setting up editor. Please try again.'),
fadeTransition: false,
addBodyClass: true,
@@ -331,7 +331,7 @@ export default {
useLivePreviewExtension();
})
.catch((e) =>
- createFlash({
+ createAlert({
message: e,
}),
);
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 10e9f6a9488..1a191f6f76f 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -99,7 +99,9 @@ export function startIde(options) {
return;
}
- if (gon.features?.vscodeWebIde) {
+ const useNewWebIde = parseBoolean(ideElement.dataset.useNewWebIde);
+
+ if (useNewWebIde) {
initGitlabWebIDE(ideElement);
} else {
resetServiceWorkersPublicPath();
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index a061da38d4f..140f2895a29 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -7,8 +7,7 @@ export const initGitlabWebIDE = async (el) => {
const baseUrl = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin);
// what: Pull what we need from the element. We will replace it soon.
- const { path_with_namespace: projectPath } = JSON.parse(el.dataset.project);
- const { cspNonce: nonce, branchName: ref } = el.dataset;
+ const { cspNonce: nonce, branchName: ref, projectPath } = el.dataset;
// what: Clean up the element, but preserve id.
// why: This way we don't inherit any `ide-loading` side-effects. This
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index b22e58a376d..dc0f3a1d7e9 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,6 +1,6 @@
import { escape } from 'lodash';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import {
@@ -36,7 +36,7 @@ export const createTempEntry = (
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (getters.entryExists(name)) {
- createFlash({
+ createAlert({
message: sprintf(__('The name "%{name}" is already taken in this directory.'), {
name: name.split('/').pop(),
}),
@@ -281,7 +281,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
if (e.response.status === 404) {
reject(e);
} else {
- createFlash({
+ createAlert({
message: __('Error loading branch data. Please try again.'),
fadeTransition: false,
addBodyClass: true,
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index f3f603d4ae9..cd8088bf667 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '../../constants';
import service from '../../services';
@@ -34,7 +34,7 @@ export const getMergeRequestsForBranch = (
}
})
.catch((e) => {
- createFlash({
+ createAlert({
message: __(`Error fetching merge requests for ${branchId}`),
fadeTransition: false,
addBodyClass: true,
@@ -233,7 +233,7 @@ export const openMergeRequest = async (
await dispatch('openMergeRequestChanges', changes);
} catch (e) {
- createFlash({ message: __('Error while loading the merge request. Please try again.') });
+ createAlert({ message: __('Error while loading the merge request. Please try again.') });
throw e;
}
};
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 37a405e3fac..7a6a267e7d0 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,5 +1,5 @@
import { escape } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import { logError } from '~/lib/logger';
import api from '~/api';
@@ -11,7 +11,7 @@ const ERROR_LOADING_PROJECT = __('Error loading project data. Please try again.'
const errorFetchingData = (e) => {
logError(ERROR_LOADING_PROJECT, e);
- createFlash({
+ createAlert({
message: ERROR_LOADING_PROJECT,
fadeTransition: false,
addBodyClass: true,
@@ -51,7 +51,7 @@ export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {})
});
})
.catch((e) => {
- createFlash({
+ createAlert({
message: __('Error loading last commit.'),
fadeTransition: false,
addBodyClass: true,
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 2ff71523b1b..cbc6e0fe519 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { addNumericSuffix } from '~/ide/utils';
import { sprintf, __ } from '~/locale';
import { leftSidebarViews } from '../../../constants';
@@ -143,7 +143,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(types.UPDATE_LOADING, false);
if (!data.short_id) {
- createFlash({
+ createAlert({
message: data.message,
fadeTransition: false,
addBodyClass: true,
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
index 82d9300d30b..91868132a5a 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import * as terminalService from '../../../../services/terminals';
@@ -26,7 +26,7 @@ export const receiveStartSessionSuccess = ({ commit, dispatch }, data) => {
};
export const receiveStartSessionError = ({ dispatch }) => {
- createFlash({ message: messages.UNEXPECTED_ERROR_STARTING });
+ createAlert({ message: messages.UNEXPECTED_ERROR_STARTING });
dispatch('killSession');
};
@@ -59,7 +59,7 @@ export const receiveStopSessionSuccess = ({ dispatch }) => {
};
export const receiveStopSessionError = ({ dispatch }) => {
- createFlash({ message: messages.UNEXPECTED_ERROR_STOPPING });
+ createAlert({ message: messages.UNEXPECTED_ERROR_STOPPING });
dispatch('killSession');
};
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js
index 7fe1a8cc2df..4aa0768d394 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as messages from '../messages';
import * as types from '../mutation_types';
@@ -42,7 +42,7 @@ export const receiveSessionStatusSuccess = ({ commit, dispatch }, data) => {
};
export const receiveSessionStatusError = ({ dispatch }) => {
- createFlash({ message: messages.UNEXPECTED_ERROR_STATUS });
+ createAlert({ message: messages.UNEXPECTED_ERROR_STATUS });
dispatch('killSession');
};
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index a7e6506b045..83a3d7f2ac3 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -1,5 +1,6 @@
import { flatten, isString } from 'lodash';
import { languages } from 'monaco-editor';
+import { setDiagnosticsOptions as yamlDiagnosticsOptions } from 'monaco-yaml';
import { performanceMarkAndMeasure } from '~/performance/utils';
import { SIDE_LEFT, SIDE_RIGHT } from './constants';
@@ -82,17 +83,16 @@ export function registerLanguages(def, ...defs) {
}
export function registerSchema(schema, options = {}) {
- const defaults = [languages.json.jsonDefaults, languages.yaml.yamlDefaults];
- defaults.forEach((d) =>
- d.setDiagnosticsOptions({
- validate: true,
- enableSchemaRequest: true,
- hover: true,
- completion: true,
- schemas: [schema],
- ...options,
- }),
- );
+ const defaultOptions = {
+ validate: true,
+ enableSchemaRequest: true,
+ hover: true,
+ completion: true,
+ schemas: [schema],
+ ...options,
+ };
+ languages.json.jsonDefaults.setDiagnosticsOptions(defaultOptions);
+ yamlDiagnosticsOptions(defaultOptions);
}
export const otherSide = (side) => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);
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 98ee858ca91..0cdd64b1b98 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
@@ -12,7 +12,7 @@ import {
GlFormCheckbox,
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__, __, n__, sprintf } from '~/locale';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
@@ -150,6 +150,10 @@ export default {
},
groupsTableData() {
+ if (!this.availableNamespaces) {
+ return [];
+ }
+
return this.groups.map((group) => {
const importTarget = this.getImportTarget(group);
const status = this.getStatus(group);
@@ -232,6 +236,10 @@ export default {
version: this.bulkImportSourceGroups.versionValidation.features.sourceInstanceVersion,
});
},
+
+ pageInfo() {
+ return this.bulkImportSourceGroups?.pageInfo ?? {};
+ },
},
watch: {
@@ -342,7 +350,7 @@ export default {
variables: { importRequests },
});
} catch (error) {
- createFlash({
+ createAlert({
message: i18n.ERROR_IMPORT,
captureError: true,
error,
@@ -503,6 +511,7 @@ export default {
permissionsHelpPath: helpPagePath('user/permissions', { anchor: 'group-members-permissions' }),
popoverOptions: { title: __('What is listed here?') },
i18n,
+ LOCAL_STORAGE_KEY: 'gl-bulk-imports-status-page-size-v1',
};
</script>
@@ -696,14 +705,15 @@ export default {
/>
</template>
</gl-table>
- <pagination-bar
- v-if="hasGroups"
- :page-info="bulkImportSourceGroups.pageInfo"
- class="gl-mt-3"
- @set-page="setPage"
- @set-page-size="setPageSize"
- />
</template>
</template>
+ <pagination-bar
+ v-show="!$apollo.loading && hasGroups"
+ :page-info="pageInfo"
+ class="gl-mt-3"
+ :storage-key="$options.LOCAL_STORAGE_KEY"
+ @set-page="setPage"
+ @set-page-size="setPageSize"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
index ba0f2bb947a..6ad5e448a40 100644
--- a/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
+++ b/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { s__ } from '~/locale';
@@ -15,7 +15,7 @@ export class StatusPoller {
statuses.forEach((status) => updateImportStatus(status));
},
errorCallback: () =>
- createFlash({
+ createAlert({
message: s__('BulkImport|Update of import statuses with realtime changes failed'),
}),
});
diff --git a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
new file mode 100644
index 00000000000..a8fdf9b9ec5
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlAccordion, GlAccordionItem, GlAlert, GlForm, GlFormCheckbox } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlAccordion,
+ GlAccordionItem,
+ GlAlert,
+ GlForm,
+ GlFormCheckbox,
+ },
+ props: {
+ stages: {
+ required: true,
+ type: Array,
+ },
+ value: {
+ required: true,
+ type: Object,
+ },
+ isInitiallyExpanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+<template>
+ <gl-accordion :header-level="3">
+ <gl-accordion-item
+ :title="s__('ImportProjects|Advanced import settings')"
+ :visible="isInitiallyExpanded"
+ >
+ <gl-alert variant="warning" class="gl-mb-5" :dismissible="false">{{
+ s__('ImportProjects|The more information you select, the longer it will take to import')
+ }}</gl-alert>
+ <gl-form>
+ <gl-form-checkbox
+ v-for="{ name, label, details } in stages"
+ :key="name"
+ :checked="value[name]"
+ @change="$emit('input', { ...value, [name]: $event })"
+ >
+ {{ label }}
+ <template v-if="details" #help>{{ details }} </template>
+ </gl-form-checkbox>
+ </gl-form>
+ </gl-accordion-item>
+ </gl-accordion>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 848c7361601..97a7ed4bf55 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -9,10 +9,12 @@ import {
import { mapActions, mapState, mapGetters } from 'vuex';
import { n__, __, sprintf } from '~/locale';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
+import AdvancedSettings from './advanced_settings.vue';
export default {
name: 'ImportProjectsTable',
components: {
+ AdvancedSettings,
ProviderRepoTableRow,
GlLoadingIcon,
GlButton,
@@ -35,6 +37,24 @@ export default {
required: false,
default: false,
},
+ optionalStages: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ isAdvancedSettingsPanelInitiallyExpanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+
+ data() {
+ return {
+ optionalStagesSelection: Object.fromEntries(
+ this.optionalStages.map(({ name }) => [name, false]),
+ ),
+ };
},
computed: {
@@ -127,7 +147,7 @@ export default {
modal-id="import-all-modal"
:title="s__('ImportProjects|Import repositories')"
:ok-title="__('Import')"
- @ok="importAll"
+ @ok="importAll({ optionalStages: optionalStagesSelection })"
>
{{
n__(
@@ -150,6 +170,13 @@ export default {
/>
</form>
</div>
+ <advanced-settings
+ v-if="optionalStages && optionalStages.length"
+ v-model="optionalStagesSelection"
+ :stages="optionalStages"
+ :is-initially-expanded="isAdvancedSettingsPanelInitiallyExpanded"
+ class="gl-mb-5"
+ />
<div v-if="repositories.length" class="gl-w-full">
<table>
<thead class="gl-border-0 gl-border-solid gl-border-t-1 gl-border-gray-100">
@@ -171,6 +198,7 @@ export default {
:repo="repo"
:available-namespaces="namespaces"
:user-namespace="defaultTargetNamespace"
+ :optional-stages="optionalStagesSelection"
/>
</template>
</tbody>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index e4090a378e1..458e0fb1cb1 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -43,6 +43,10 @@ export default {
type: Array,
required: true,
},
+ optionalStages: {
+ type: Object,
+ required: true,
+ },
},
computed: {
@@ -177,7 +181,7 @@ export default {
v-if="isImportNotStarted"
type="button"
data-qa-selector="import_button"
- @click="fetchImport(repo.importSource.id)"
+ @click="fetchImport({ repoId: repo.importSource.id, optionalStages })"
>
{{ importButtonText }}
</gl-button>
diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js
index 5146a0eb461..4daa9e8a1b8 100644
--- a/app/assets/javascripts/import_entities/import_projects/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/index.js
@@ -42,6 +42,7 @@ export function initPropsFromElement(element) {
providerTitle: element.dataset.provider,
filterable: parseBoolean(element.dataset.filterable),
paginatable: parseBoolean(element.dataset.paginatable),
+ optionalStages: JSON.parse(element.dataset.optionalStages),
};
}
diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js
index 92be028b8a9..a30c14f9d28 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -43,11 +43,14 @@ const restartJobsPolling = () => {
const setImportTarget = ({ commit }, { repoId, importTarget }) =>
commit(types.SET_IMPORT_TARGET, { repoId, importTarget });
-const importAll = ({ state, dispatch }) => {
+const importAll = ({ state, dispatch }, config = {}) => {
return Promise.all(
- state.repositories
- .filter(isProjectImportable)
- .map((r) => dispatch('fetchImport', r.importSource.id)),
+ state.repositories.filter(isProjectImportable).map((r) =>
+ dispatch('fetchImport', {
+ repoId: r.importSource.id,
+ optionalStages: config?.optionalStages,
+ }),
+ ),
);
};
@@ -73,7 +76,7 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else if (tooManyRequests(e)) {
- createFlash({
+ createAlert({
message: sprintf(s__('ImportProjects|%{provider} rate limit exceeded. Try again later'), {
provider: capitalizeFirstCharacter(provider),
}),
@@ -81,7 +84,7 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
commit(types.RECEIVE_REPOS_ERROR);
} else {
- createFlash({
+ createAlert({
message: sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), {
provider,
}),
@@ -92,7 +95,10 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
});
};
-const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, getters }, repoId) => {
+const fetchImportFactory = (importPath = isRequired()) => (
+ { state, commit, getters },
+ { repoId, optionalStages },
+) => {
const { ciCdOnly } = state;
const importTarget = getters.getImportTarget(repoId);
@@ -105,6 +111,7 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett
ci_cd_only: ciCdOnly,
new_name: newName,
target_namespace: targetNamespace,
+ ...(Object.keys(optionalStages).length ? { optional_stages: optionalStages } : {}),
})
.then(({ data }) => {
commit(types.RECEIVE_IMPORT_SUCCESS, {
@@ -124,7 +131,7 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett
)
: s__('ImportProjects|Importing the project failed');
- createFlash({
+ createAlert({
message: flashMessage,
});
@@ -149,7 +156,7 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else {
- createFlash({
+ createAlert({
message: s__('ImportProjects|Update of imported projects with realtime changes failed'),
});
}
@@ -177,7 +184,7 @@ const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) =
commit(types.RECEIVE_NAMESPACES_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
)
.catch(() => {
- createFlash({
+ createAlert({
message: s__('ImportProjects|Requesting namespaces failed'),
});
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index 437bcc39886..2806b785816 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -91,3 +91,5 @@ export const placeholderForType = {
[INTEGRATION_TYPE_SLACK]: __('#general, #development'),
[INTEGRATION_TYPE_MATTERMOST]: __('my-channel'),
};
+
+export const INTEGRATION_FORM_TYPE_SLACK = 'gitlab_slack_application';
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 7a6f1a953a8..15f76c16516 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -14,12 +14,14 @@ import {
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
+ INTEGRATION_FORM_TYPE_SLACK,
integrationLevels,
integrationFormSectionComponents,
billingPlanNames,
} from '~/integrations/constants';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import csrf from '~/lib/utils/csrf';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { testIntegrationSettings } from '../api';
import ActiveCheckbox from './active_checkbox.vue';
import ConfirmationModal from './confirmation_modal.vue';
@@ -65,6 +67,7 @@ export default {
GlModal: GlModalDirective,
SafeHtml,
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
helpHtml: {
default: '',
@@ -101,6 +104,9 @@ export default {
return Boolean(this.isSaving || this.isResetting || this.isTesting);
},
hasSections() {
+ if (this.hasSlackNotificationsDisabled) {
+ return false;
+ }
return this.customState.sections.length !== 0;
},
fieldsWithoutSection() {
@@ -108,6 +114,24 @@ export default {
? this.propsSource.fields.filter((field) => !field.section)
: this.propsSource.fields;
},
+ hasFieldsWithoutSection() {
+ if (this.hasSlackNotificationsDisabled) {
+ return false;
+ }
+ return this.fieldsWithoutSection.length;
+ },
+ isSlackIntegration() {
+ return this.propsSource.type === INTEGRATION_FORM_TYPE_SLACK;
+ },
+ hasSlackNotificationsDisabled() {
+ return this.isSlackIntegration && !this.glFeatures.integrationSlackAppNotifications;
+ },
+ showHelpHtml() {
+ if (this.isSlackIntegration) {
+ return this.helpHtml;
+ }
+ return !this.hasSections && this.helpHtml;
+ },
},
methods: {
...mapActions(['setOverride', 'requestJiraIssueTypes']),
@@ -227,6 +251,31 @@ export default {
@change="setOverride"
/>
+ <section v-if="showHelpHtml" class="gl-lg-display-flex gl-justify-content-end gl-mb-6">
+ <!-- helpHtml is trusted input -->
+ <div
+ v-safe-html:[$options.helpHtmlConfig]="helpHtml"
+ data-testid="help-html"
+ class="gl-flex-basis-two-thirds"
+ ></div>
+ </section>
+
+ <section v-if="!hasSections" class="gl-lg-display-flex gl-justify-content-end">
+ <div class="gl-flex-basis-two-thirds">
+ <active-checkbox
+ v-if="propsSource.showActive"
+ :key="`${currentKey}-active-checkbox`"
+ @toggle-integration-active="onToggleIntegrationState"
+ />
+ <trigger-fields
+ v-if="propsSource.triggerEvents.length"
+ :key="`${currentKey}-trigger-fields`"
+ :events="propsSource.triggerEvents"
+ :type="propsSource.type"
+ />
+ </div>
+ </section>
+
<template v-if="hasSections">
<div
v-for="(section, index) in customState.sections"
@@ -234,8 +283,8 @@ export default {
:class="{ 'gl-border-b gl-pb-3 gl-mb-6': index !== customState.sections.length - 1 }"
data-testid="integration-section"
>
- <div class="row">
- <div class="col-lg-4">
+ <section class="gl-lg-display-flex">
+ <div class="gl-flex-basis-third gl-mr-4">
<h4 class="gl-mt-0">
{{ section.title
}}<gl-badge
@@ -253,7 +302,7 @@ export default {
<p v-safe-html="section.description"></p>
</div>
- <div class="col-lg-8">
+ <div class="gl-flex-basis-two-thirds">
<component
:is="$options.integrationFormSectionComponents[section.type]"
:fields="fieldsForSection(section)"
@@ -262,28 +311,12 @@ export default {
@request-jira-issue-types="onRequestJiraIssueTypes"
/>
</div>
- </div>
+ </section>
</div>
</template>
- <div class="row">
- <div class="col-lg-4"></div>
-
- <div class="col-lg-8">
- <!-- helpHtml is trusted input -->
- <div v-if="helpHtml && !hasSections" v-safe-html:[$options.helpHtmlConfig]="helpHtml"></div>
-
- <active-checkbox
- v-if="propsSource.showActive && !hasSections"
- :key="`${currentKey}-active-checkbox`"
- @toggle-integration-active="onToggleIntegrationState"
- />
- <trigger-fields
- v-if="propsSource.triggerEvents.length && !hasSections"
- :key="`${currentKey}-trigger-fields`"
- :events="propsSource.triggerEvents"
- :type="propsSource.type"
- />
+ <section v-if="hasFieldsWithoutSection" class="gl-lg-display-flex gl-justify-content-end">
+ <div class="gl-flex-basis-two-thirds">
<dynamic-field
v-for="field in fieldsWithoutSection"
:key="`${currentKey}-${field.name}`"
@@ -292,12 +325,12 @@ export default {
:data-qa-selector="`${field.name}_div`"
/>
</div>
- </div>
+ </section>
- <div v-if="isEditable" class="row">
- <div :class="hasSections ? 'col' : 'col-lg-8 offset-lg-4'">
+ <section v-if="isEditable" :class="!hasSections && 'gl-lg-display-flex gl-justify-content-end'">
+ <div :class="!hasSections && 'gl-flex-basis-two-thirds'">
<div
- class="footer-block row-content-block gl-display-flex gl-justify-content-space-between"
+ class="footer-block row-content-block gl-lg-display-flex gl-justify-content-space-between"
>
<div>
<template v-if="isInstanceOrGroupLevel">
@@ -359,6 +392,6 @@ export default {
</template>
</div>
</div>
- </div>
+ </section>
</gl-form>
</template>
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
deleted file mode 100644
index 243d82f55aa..00000000000
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import $ from 'jquery';
-import { loadCSSFile } from '../lib/utils/css_utils';
-
-let instanceCount = 0;
-
-class AutoWidthDropdownSelect {
- constructor(selectElement) {
- this.$selectElement = $(selectElement);
- this.dropdownClass = `js-auto-width-select-dropdown-${instanceCount}`;
- instanceCount += 1;
- }
-
- init() {
- const { dropdownClass } = this;
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- // eslint-disable-next-line promise/no-nesting
- loadCSSFile(gon.select2_css_path)
- .then(() => {
- this.$selectElement.select2({
- dropdownCssClass: dropdownClass,
- ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
- });
- })
- .catch(() => {});
- })
- .catch(() => {});
-
- return this;
- }
-
- static selectOptions(dropdownClass) {
- return {
- dropdownCss() {
- let resultantWidth = 'auto';
- const $dropdown = $(`.${dropdownClass}`);
-
- // We have to look at the parent because
- // `offsetParent` on a `display: none;` is `null`
- const offsetParentWidth = $(this).parent().offsetParent().width();
- // Reset any width to let it naturally flow
- $dropdown.css('width', 'auto');
- if ($dropdown.outerWidth(false) > offsetParentWidth) {
- resultantWidth = offsetParentWidth;
- }
-
- return {
- width: resultantWidth,
- maxWidth: offsetParentWidth,
- };
- },
- };
- }
-}
-
-export default AutoWidthDropdownSelect;
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue b/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue
index 9509399e91d..ba94932289e 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue
@@ -1,10 +1,9 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
-import { ISSUE_STATUS_SELECT_OPTIONS } from '../constants';
+import { statusDropdownOptions } from '../constants';
export default {
- name: 'StatusSelect',
components: {
GlDropdown,
GlDropdownItem,
@@ -36,7 +35,7 @@ export default {
dropdownTitle: __('Change status'),
defaultDropdownText: __('Select status'),
},
- ISSUE_STATUS_SELECT_OPTIONS,
+ statusDropdownOptions,
};
</script>
<template>
@@ -44,7 +43,7 @@ export default {
<input type="hidden" name="update[state_event]" :value="selectedValue" />
<gl-dropdown :text="dropdownText" :title="$options.i18n.dropdownTitle" class="gl-w-full">
<gl-dropdown-item
- v-for="statusOption in $options.ISSUE_STATUS_SELECT_OPTIONS"
+ v-for="statusOption in $options.statusDropdownOptions"
:key="statusOption.value"
:is-checked="selectedValue === statusOption.value"
is-check-item
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue b/app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue
new file mode 100644
index 00000000000..8774b065c22
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { subscriptionsDropdownOptions } from '../constants';
+
+export default {
+ subscriptionsDropdownOptions,
+ i18n: {
+ defaultDropdownText: __('Select subscription'),
+ headerText: __('Change subscription'),
+ },
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ data() {
+ return {
+ subscription: undefined,
+ };
+ },
+ computed: {
+ dropdownText() {
+ return this.subscription?.text ?? this.$options.i18n.defaultDropdownText;
+ },
+ selectedValue() {
+ return this.subscription?.value;
+ },
+ },
+ methods: {
+ handleClick(option) {
+ this.subscription = option.value === this.subscription?.value ? undefined : option;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <input type="hidden" name="update[subscription_event]" :value="selectedValue" />
+ <gl-dropdown class="gl-w-full" :header-text="$options.i18n.headerText" :text="dropdownText">
+ <gl-dropdown-item
+ v-for="subscriptionsOption in $options.subscriptionsDropdownOptions"
+ :key="subscriptionsOption.value"
+ is-check-item
+ :is-checked="selectedValue === subscriptionsOption.value"
+ @click="handleClick(subscriptionsOption)"
+ >
+ {{ subscriptionsOption.text }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js b/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
index ad15b25f9cf..68133ceb3c7 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
@@ -1,17 +1,23 @@
import { __ } from '~/locale';
-export const ISSUE_STATUS_MODIFIERS = {
- REOPEN: 'reopen',
- CLOSE: 'close',
-};
-
-export const ISSUE_STATUS_SELECT_OPTIONS = [
+export const statusDropdownOptions = [
{
- value: ISSUE_STATUS_MODIFIERS.REOPEN,
text: __('Open'),
+ value: 'reopen',
},
{
- value: ISSUE_STATUS_MODIFIERS.CLOSE,
text: __('Closed'),
+ value: 'close',
+ },
+];
+
+export const subscriptionsDropdownOptions = [
+ {
+ text: __('Subscribe'),
+ value: 'subscribe',
+ },
+ {
+ text: __('Unsubscribe'),
+ value: 'unsubscribe',
},
];
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
index 967996b859e..4657771353f 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
-import StatusSelect from './components/status_select.vue';
+import StatusDropdown from './components/status_dropdown.vue';
+import SubscriptionsDropdown from './components/subscriptions_dropdown.vue';
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
@@ -14,8 +15,8 @@ export function initBulkUpdateSidebar(prefixId) {
new IssuableBulkUpdateSidebar(); // eslint-disable-line no-new
}
-export function initIssueStatusSelect() {
- const el = document.querySelector('.js-issue-status');
+export function initStatusDropdown() {
+ const el = document.querySelector('.js-status-dropdown');
if (!el) {
return null;
@@ -23,7 +24,21 @@ export function initIssueStatusSelect() {
return new Vue({
el,
- name: 'StatusSelectRoot',
- render: (createElement) => createElement(StatusSelect),
+ name: 'StatusDropdownRoot',
+ render: (createElement) => createElement(StatusDropdown),
+ });
+}
+
+export function initSubscriptionsDropdown() {
+ const el = document.querySelector('.js-subscriptions-dropdown');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'SubscriptionsDropdownRoot',
+ render: (createElement) => createElement(SubscriptionsDropdown),
});
}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
index 8a55176fed0..a33c6ae8030 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
@@ -5,7 +5,6 @@ import issuableEventHub from '~/issues/list/eventhub';
import LabelsSelect from '~/labels/labels_select';
import MilestoneSelect from '~/milestones/milestone_select';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
-import subscriptionSelect from './subscription_select';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -52,7 +51,6 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
- subscriptionSelect();
// Checking IS_EE and using ee_else_ce is odd, but we do it here to satisfy
// the import/no-unresolved lint rule when FOSS_ONLY=1, even though at
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js b/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js
deleted file mode 100644
index b12ac776b4f..00000000000
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import $ from 'jquery';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { __ } from '~/locale';
-
-export default function subscriptionSelect() {
- $('.js-subscription-event').each((i, element) => {
- const fieldName = $(element).data('fieldName');
-
- return initDeprecatedJQueryDropdown($(element), {
- selectable: true,
- fieldName,
- toggleLabel(selected, el, instance) {
- let label = __('Subscription');
- const $item = instance.dropdown.find('.is-active');
- if ($item.length) {
- label = $item.text();
- }
- return label;
- },
- clicked(options) {
- return options.e.preventDefault();
- },
- id(obj, el) {
- return $(el).data('id');
- },
- });
- });
-}
diff --git a/app/assets/javascripts/issuable/issuable_context.js b/app/assets/javascripts/issuable/issuable_context.js
index 37001d00a27..8c2e2a5df67 100644
--- a/app/assets/javascripts/issuable/issuable_context.js
+++ b/app/assets/javascripts/issuable/issuable_context.js
@@ -1,7 +1,6 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
-import { loadCSSFile } from '~/lib/utils/css_utils';
import UsersSelect from '~/users_select';
export default class IssuableContext {
@@ -9,24 +8,6 @@ export default class IssuableContext {
this.userSelect = new UsersSelect(currentUser);
this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
- const $select2 = $('select.select2');
-
- if ($select2.length) {
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- // eslint-disable-next-line promise/no-nesting
- loadCSSFile(gon.select2_css_path)
- .then(() => {
- $select2.select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
- })
- .catch(() => {});
- })
- .catch(() => {});
- }
-
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
});
diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js
index 81bf7ca6ccc..e8ba99e0e9e 100644
--- a/app/assets/javascripts/issuable/issuable_form.js
+++ b/app/assets/javascripts/issuable/issuable_form.js
@@ -2,10 +2,7 @@ import $ from 'jquery';
import Pikaday from 'pikaday';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import Autosave from '~/autosave';
-import AutoWidthDropdownSelect from '~/issuable/auto_width_dropdown_select';
-import { loadCSSFile } from '~/lib/utils/css_utils';
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
-import { select2AxiosTransport } from '~/lib/utils/select2_utils';
import { queryToObject, objectToQuery } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select';
import ZenMode from '~/zen_mode';
@@ -118,12 +115,6 @@ export default class IssuableForm {
});
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
-
- this.$targetBranchSelect = $('.js-target-branch-select', this.form);
-
- if (this.$targetBranchSelect.length) {
- this.initTargetBranchDropdown();
- }
}
initAutosave() {
@@ -214,47 +205,4 @@ export default class IssuableForm {
addWip() {
this.titleField.val(`Draft: ${this.titleField.val()}`);
}
-
- initTargetBranchDropdown() {
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- // eslint-disable-next-line promise/no-nesting
- loadCSSFile(gon.select2_css_path)
- .then(() => {
- this.$targetBranchSelect.select2({
- ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
- ajax: {
- url: this.$targetBranchSelect.data('endpoint'),
- dataType: 'JSON',
- quietMillis: 250,
- data(search) {
- return {
- search,
- };
- },
- results({ results }) {
- return {
- // `data` keys are translated so we can't just access them with a string based key
- results: results[Object.keys(results)[0]].map((name) => ({
- id: name,
- text: name,
- })),
- };
- },
- transport: select2AxiosTransport,
- },
- initSelection(el, callback) {
- const val = el.val();
-
- callback({
- id: val,
- text: val,
- });
- },
- });
- })
- .catch(() => {});
- })
- .catch(() => {});
- }
}
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 0b424d105b9..acb6aa93f0f 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -247,8 +247,8 @@ export default {
},
defaultWorkItemTypes() {
return this.isWorkItemsEnabled
- ? defaultWorkItemTypes.concat(WORK_ITEM_TYPE_ENUM_TASK)
- : defaultWorkItemTypes;
+ ? defaultWorkItemTypes
+ : defaultWorkItemTypes.filter((type) => type !== WORK_ITEM_TYPE_ENUM_TASK);
},
typeTokenOptions() {
return this.isWorkItemsEnabled
@@ -563,7 +563,8 @@ export default {
if (!this.hasInitBulkEdit) {
const bulkUpdateSidebar = await import('~/issuable/bulk_update_sidebar');
bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
- bulkUpdateSidebar.initIssueStatusSelect();
+ bulkUpdateSidebar.initStatusDropdown();
+ bulkUpdateSidebar.initSubscriptionsDropdown();
const usersSelect = await import('~/users_select');
const UsersSelect = usersSelect.default;
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 27738d7a3e6..9fe8899ab39 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -7,11 +7,13 @@ import {
FILTER_UPCOMING,
OPERATOR_IS,
OPERATOR_IS_NOT,
+ TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
WORK_ITEM_TYPE_ENUM_INCIDENT,
WORK_ITEM_TYPE_ENUM_ISSUE,
WORK_ITEM_TYPE_ENUM_TEST_CASE,
+ WORK_ITEM_TYPE_ENUM_TASK,
} from '~/work_items/constants';
export const i18n = {
@@ -147,14 +149,16 @@ export const TOKEN_TYPE_EPIC = 'epic_id';
export const TOKEN_TYPE_WEIGHT = 'weight';
export const TOKEN_TYPE_CONTACT = 'crm_contact';
export const TOKEN_TYPE_ORGANIZATION = 'crm_organization';
-export const TOKEN_TYPE_HEALTH = 'health_status';
-export const TYPE_TOKEN_TASK_OPTION = { icon: 'task-done', title: 'task', value: 'task' };
+export const TYPE_TOKEN_TASK_OPTION = { icon: 'issue-type-task', title: 'task', value: 'task' };
+// This should be consistent with Issue::TYPES_FOR_LIST in the backend
+// https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/models/issue.rb#L48
export const defaultWorkItemTypes = [
WORK_ITEM_TYPE_ENUM_ISSUE,
WORK_ITEM_TYPE_ENUM_INCIDENT,
WORK_ITEM_TYPE_ENUM_TEST_CASE,
+ WORK_ITEM_TYPE_ENUM_TASK,
];
export const defaultTypeTokenOptions = [
@@ -327,10 +331,12 @@ export const filters = {
[TOKEN_TYPE_HEALTH]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'healthStatus',
+ [SPECIAL_FILTER]: 'healthStatus',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'health_status',
+ [SPECIAL_FILTER]: 'health_status',
},
},
},
diff --git a/app/assets/javascripts/issues/show/components/edited.vue b/app/assets/javascripts/issues/show/components/edited.vue
index 4c5f783cd66..5138a4530e9 100644
--- a/app/assets/javascripts/issues/show/components/edited.vue
+++ b/app/assets/javascripts/issues/show/components/edited.vue
@@ -1,10 +1,11 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
+import { GlSprintf } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
TimeAgoTooltip,
+ GlSprintf,
},
props: {
updatedAt: {
@@ -33,13 +34,27 @@ export default {
<template>
<small class="edited-text js-issue-widgets">
- Edited
- <time-ago-tooltip v-if="updatedAt" :time="updatedAt" tooltip-placement="bottom" />
- <span v-if="hasUpdatedBy">
- by
- <a :href="updatedByPath" class="author-link">
- <span>{{ updatedByName }}</span>
- </a>
- </span>
+ <gl-sprintf v-if="!hasUpdatedBy" :message="__('Edited %{timeago}')">
+ <template #timeago>
+ <time-ago-tooltip :time="updatedAt" tooltip-placement="bottom" />
+ </template>
+ </gl-sprintf>
+ <gl-sprintf v-else-if="!updatedAt" :message="__('Edited by %{author}')">
+ <template #author>
+ <a :href="updatedByPath" class="author-link gl-hover-text-decoration-underline">
+ <span>{{ updatedByName }}</span>
+ </a>
+ </template>
+ </gl-sprintf>
+ <gl-sprintf v-else :message="__('Edited %{timeago} by %{author}')">
+ <template #timeago>
+ <time-ago-tooltip :time="updatedAt" tooltip-placement="bottom" />
+ </template>
+ <template #author>
+ <a :href="updatedByPath" class="author-link gl-hover-text-decoration-underline">
+ <span>{{ updatedByName }}</span>
+ </a>
+ </template>
+ </gl-sprintf>
</small>
</template>
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index c2ab7c4f298..dbe634e7295 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -1,13 +1,16 @@
<script>
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import updateMixin from '../../mixins/update';
export default {
components: {
MarkdownField,
+ MarkdownEditor,
},
- mixins: [updateMixin],
+ mixins: [updateMixin, glFeaturesFlagMixin()],
props: {
value: {
type: String,
@@ -38,7 +41,12 @@ export default {
},
},
mounted() {
- this.$refs.textarea.focus();
+ this.focus();
+ },
+ methods: {
+ focus() {
+ this.$refs.textarea?.focus();
+ },
},
};
</script>
@@ -46,7 +54,26 @@ export default {
<template>
<div class="common-note-form">
<label class="sr-only" for="issue-description">{{ __('Description') }}</label>
+ <markdown-editor
+ v-if="glFeatures.contentEditorOnIssues"
+ class="gl-mt-3"
+ :value="value"
+ :render-markdown-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :form-field-aria-label="__('Description')"
+ :form-field-placeholder="__('Write a comment or drag your files here…')"
+ form-field-id="issue-description"
+ form-field-name="issue-description"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :enable-autocomplete="enableAutocomplete"
+ supports-quick-actions
+ init-on-autofocus
+ @input="$emit('input', $event)"
+ @keydown.meta.enter="updateIssuable"
+ @keydown.ctrl.enter="updateIssuable"
+ />
<markdown-field
+ v-else
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue
index f479c8ae78d..0c6b61fb893 100644
--- a/app/assets/javascripts/issues/show/components/form.vue
+++ b/app/assets/javascripts/issues/show/components/form.vue
@@ -1,7 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import $ from 'jquery';
-import Autosave from '~/autosave';
+import { getDraft, updateDraft, getLockVersion, clearDraft } from '~/lib/utils/autosave';
import { IssuableType } from '~/issues/constants';
import eventHub from '../event_hub';
import EditActions from './edit_actions.vue';
@@ -76,10 +75,17 @@ export default {
},
},
data() {
+ const autosaveKey = [document.location.pathname, document.location.search];
+ const descriptionAutosaveKey = [...autosaveKey, 'description'];
+ const titleAutosaveKey = [...autosaveKey, 'title'];
+
return {
+ titleAutosaveKey,
+ descriptionAutosaveKey,
+ autosaveReset: false,
formData: {
- title: this.formState.title,
- description: this.formState.description,
+ title: getDraft(titleAutosaveKey) || this.formState.title,
+ description: getDraft(descriptionAutosaveKey) || this.formState.description,
},
showOutdatedDescriptionWarning: false,
};
@@ -118,58 +124,40 @@ export default {
},
methods: {
initAutosave() {
- const {
- description: {
- $refs: { textarea },
- },
- title: {
- $refs: { input },
- },
- } = this.$refs;
-
- this.autosaveDescription = new Autosave(
- $(textarea),
- [document.location.pathname, document.location.search, 'description'],
- null,
- this.formState.lock_version,
- );
-
- const savedLockVersion = this.autosaveDescription.getSavedLockVersion();
+ const savedLockVersion = getLockVersion(this.descriptionAutosaveKey);
this.showOutdatedDescriptionWarning =
savedLockVersion && String(this.formState.lock_version) !== savedLockVersion;
-
- this.autosaveTitle = new Autosave($(input), [
- document.location.pathname,
- document.location.search,
- 'title',
- ]);
},
resetAutosave() {
- this.autosaveDescription.reset();
- this.autosaveTitle.reset();
+ this.autosaveReset = true;
+ clearDraft(this.descriptionAutosaveKey);
+ clearDraft(this.titleAutosaveKey);
},
keepAutosave() {
- const {
- description: {
- $refs: { textarea },
- },
- } = this.$refs;
-
- textarea.focus();
+ this.$refs.description.focus();
this.showOutdatedDescriptionWarning = false;
},
discardAutosave() {
- const {
- description: {
- $refs: { textarea },
- },
- } = this.$refs;
-
- textarea.value = this.initialDescriptionText;
- textarea.focus();
+ this.formData.description = this.initialDescriptionText;
+ clearDraft(this.descriptionAutosaveKey);
+ this.$refs.description.focus();
this.showOutdatedDescriptionWarning = false;
},
+ updateTitleDraft(title) {
+ updateDraft(this.titleAutosaveKey, title);
+ },
+ updateDescriptionDraft(description) {
+ /*
+ * This conditional statement prevents a race-condition
+ * between clearing the draft and submitting a new draft
+ * update while the user is typing. It happens when saving
+ * using the cmd + enter keyboard shortcut.
+ */
+ if (!this.autosaveReset) {
+ updateDraft(this.descriptionAutosaveKey, description, this.formState.lock_version);
+ }
+ },
},
};
</script>
@@ -194,7 +182,7 @@ export default {
>
<div class="row gl-mb-3">
<div class="col-12">
- <issuable-title-field ref="title" v-model="formData.title" />
+ <issuable-title-field ref="title" v-model="formData.title" @input="updateTitleDraft" />
</div>
</div>
<div class="row">
@@ -220,6 +208,7 @@ export default {
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
+ @input="updateDescriptionDraft"
/>
<edit-actions :endpoint="endpoint" :form-state="formState" :issuable-type="issuableType" />
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index adf449aca7b..74d166f82bb 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -229,7 +229,7 @@ export default {
</script>
<template>
- <div class="detail-page-header-actions gl-display-flex">
+ <div class="detail-page-header-actions gl-display-flex gl-align-self-start">
<gl-dropdown
v-if="hasMobileDropdown"
class="gl-sm-display-none! w-100"
diff --git a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
index dd84a1d7d67..5725d0f8d6a 100644
--- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -52,9 +52,6 @@ export default {
loading() {
return this.$apollo.queries.alert.loading;
},
- incidentTabEnabled() {
- return this.glFeatures.incidentTimeline;
- },
},
mounted() {
this.trackPageViews();
@@ -112,7 +109,7 @@ export default {
>
<alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
- <timeline-tab v-if="incidentTabEnabled" />
+ <timeline-tab />
</gl-tabs>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index b7ae18372ab..55cd8b5f606 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -74,6 +74,9 @@ export default {
return utcDate.toISOString();
},
+ hasTimelineText() {
+ return this.timelineText.length > 0;
+ },
},
mounted() {
this.focusDate();
@@ -167,6 +170,8 @@ export default {
variant="confirm"
category="primary"
class="gl-mr-3"
+ data-testid="save-button"
+ :disabled="!hasTimelineText"
:loading="isEventProcessed"
@click="handleSave(false)"
>
@@ -177,6 +182,8 @@ export default {
variant="confirm"
category="secondary"
class="gl-mr-3 gl-ml-n2"
+ data-testid="save-and-add-button"
+ :disabled="!hasTimelineText"
:loading="isEventProcessed"
@click="handleSave(true)"
>
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index e5eed9f6b79..3cb5007ab0d 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -60,6 +60,7 @@ export function initIncidentApp(issueData = {}) {
projectId,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable),
+ contentEditorOnIssues: gon.features.contentEditorOnIssues,
},
render(createElement) {
return createElement(IssueApp, {
diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/jobs/components/table/constants.js
index 853834ed51d..f73241aed6b 100644
--- a/app/assets/javascripts/jobs/components/table/constants.js
+++ b/app/assets/javascripts/jobs/components/table/constants.js
@@ -1,5 +1,4 @@
import { s__, __ } from '~/locale';
-import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
/* Error constants */
export const POST_FAILURE = 'post_failure';
@@ -29,62 +28,46 @@ export const PLAY_JOB_CONFIRMATION_MESSAGE = s__(
export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?');
/* Table constants */
-
-const defaultTableClasses = {
- tdClass: 'gl-p-5!',
- thClass: DEFAULT_TH_CLASSES,
-};
-// eslint-disable-next-line @gitlab/require-i18n-strings
-const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`;
-
export const DEFAULT_FIELDS = [
{
key: 'status',
label: __('Status'),
- ...defaultTableClasses,
columnClass: 'gl-w-10p',
},
{
key: 'job',
label: __('Job'),
- ...defaultTableClasses,
columnClass: 'gl-w-20p',
},
{
key: 'pipeline',
label: __('Pipeline'),
- ...defaultTableClasses,
columnClass: 'gl-w-10p',
},
{
key: 'stage',
label: __('Stage'),
- ...defaultTableClasses,
columnClass: 'gl-w-10p',
},
{
key: 'name',
label: __('Name'),
- ...defaultTableClasses,
columnClass: 'gl-w-15p',
},
{
key: 'duration',
label: __('Duration'),
- ...defaultTableClasses,
columnClass: 'gl-w-15p',
},
{
key: 'coverage',
label: __('Coverage'),
- tdClass: coverageTdClasses,
- thClass: defaultTableClasses.thClass,
+ tdClass: 'gl-display-none! gl-lg-display-table-cell!',
columnClass: 'gl-w-10p',
},
{
key: 'actions',
label: '',
- ...defaultTableClasses,
columnClass: 'gl-w-10p',
},
];
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
index 0a4757d11a8..3209fc4b90d 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert, GlSkeletonLoader, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility';
import JobsFilteredSearch from '../filtered_search/jobs_filtered_search.vue';
import { validateQueryString } from '../filtered_search/utils';
@@ -134,7 +134,7 @@ export default {
// when a user enters raw text we alert them that it is
// not supported and we do not make an additional API call
if (!filter.type) {
- createFlash({
+ createAlert({
message: RAW_TEXT_WARNING,
type: 'warning',
});
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index 927ba7c7e1e..272181f830c 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { setFaviconOverlay, resetFavicon } from '~/lib/utils/favicon';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -100,7 +100,7 @@ export const receiveJobSuccess = ({ commit }, data = {}) => {
};
export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR);
- createFlash({
+ createAlert({
message: __('An error occurred while fetching the job.'),
});
resetFavicon();
@@ -205,14 +205,14 @@ export const receiveJobLogSuccess = ({ commit }, log) => commit(types.RECEIVE_JO
export const receiveJobLogError = ({ dispatch }) => {
dispatch('stopPollingJobLog');
- createFlash({
+ createAlert({
message: __('An error occurred while fetching the job log.'),
});
};
export const receiveJobLogUnauthorizedError = ({ dispatch }) => {
dispatch('stopPollingJobLog');
- createFlash({
+ createAlert({
message: __('The current user is not authorized to access the job log.'),
});
};
@@ -254,7 +254,7 @@ export const receiveJobsForStageSuccess = ({ commit }, data) =>
export const receiveJobsForStageError = ({ commit }) => {
commit(types.RECEIVE_JOBS_FOR_STAGE_ERROR);
- createFlash({
+ createAlert({
message: __('An error occurred while fetching the jobs.'),
});
};
@@ -271,7 +271,7 @@ export const triggerManualJob = ({ state }, variables) => {
job_variables_attributes: parsedVariables,
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while triggering the job.'),
}),
);
diff --git a/app/assets/javascripts/labels/components/promote_label_modal.vue b/app/assets/javascripts/labels/components/promote_label_modal.vue
index 8598500c842..1b99a094c48 100644
--- a/app/assets/javascripts/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/labels/components/promote_label_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlSprintf, GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
@@ -70,7 +70,7 @@ export default {
labelUrl: this.url,
successful: false,
});
- createFlash({
+ createAlert({
message: error,
});
});
diff --git a/app/assets/javascripts/labels/group_label_subscription.js b/app/assets/javascripts/labels/group_label_subscription.js
index ea69e6585e6..c4f80d32a83 100644
--- a/app/assets/javascripts/labels/group_label_subscription.js
+++ b/app/assets/javascripts/labels/group_label_subscription.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { __ } from '~/locale';
import { fixTitle, hide } from '~/tooltips';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
const tooltipTitles = {
@@ -31,7 +31,7 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.removeAttr('data-url');
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was an error when unsubscribing from this label.'),
}),
);
@@ -50,7 +50,7 @@ export default class GroupLabelSubscription {
.then(() => GroupLabelSubscription.setNewTooltip($btn))
.then(() => this.toggleSubscriptionButtons())
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was an error when subscribing to this label.'),
}),
);
diff --git a/app/assets/javascripts/labels/label_manager.js b/app/assets/javascripts/labels/label_manager.js
index 1927ac6e1ec..be515869bff 100644
--- a/app/assets/javascripts/labels/label_manager.js
+++ b/app/assets/javascripts/labels/label_manager.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
import { dispose } from '~/tooltips';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -112,7 +112,7 @@ export default class LabelManager {
onPrioritySortUpdate() {
this.savePrioritySort().catch(() =>
- createFlash({
+ createAlert({
message: this.errorMessage,
}),
);
@@ -127,7 +127,7 @@ export default class LabelManager {
rollbackLabelPosition($label, originalAction) {
const action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false);
- createFlash({
+ createAlert({
message: this.errorMessage,
});
}
diff --git a/app/assets/javascripts/labels/labels_select.js b/app/assets/javascripts/labels/labels_select.js
index 51fedac339b..65dda804a20 100644
--- a/app/assets/javascripts/labels/labels_select.js
+++ b/app/assets/javascripts/labels/labels_select.js
@@ -6,7 +6,7 @@ import { difference, isEqual, escape, sortBy, template, union } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import IssuableBulkUpdateActions from '~/issuable/bulk_update_sidebar/issuable_bulk_update_actions';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { sprintf, __ } from '~/locale';
import CreateLabelDropdown from './create_label_dropdown';
@@ -146,7 +146,7 @@ export default class LabelsSelect {
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error saving label update.'),
}),
);
@@ -185,7 +185,7 @@ export default class LabelsSelect {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error fetching labels.'),
}),
);
diff --git a/app/assets/javascripts/labels/project_label_subscription.js b/app/assets/javascripts/labels/project_label_subscription.js
index b2612e9ede0..9ca6ee5609c 100644
--- a/app/assets/javascripts/labels/project_label_subscription.js
+++ b/app/assets/javascripts/labels/project_label_subscription.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { fixTitle } from '~/tooltips';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -61,7 +61,7 @@ export default class ProjectLabelSubscription {
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was an error subscribing to this label.'),
}),
);
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index 6f24590f9e7..27760e483aa 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -3,12 +3,21 @@ import { getNormalizedURL, getBaseURL, relativePathToAbsolute } from '~/lib/util
const { sanitize: dompurifySanitize, addHook, isValidAttribute } = DOMPurify;
-const defaultConfig = {
+export const defaultConfig = {
// Safely allow SVG <use> tags
ADD_TAGS: ['use', 'gl-emoji', 'copy-code'],
// Prevent possible XSS attacks with data-* attributes used by @rails/ujs
// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1421
- FORBID_ATTR: ['data-remote', 'data-url', 'data-type', 'data-method'],
+ FORBID_ATTR: [
+ 'data-remote',
+ 'data-url',
+ 'data-type',
+ 'data-method',
+ 'data-disable-with',
+ 'data-disabled',
+ 'data-disable',
+ 'data-turbo',
+ ],
FORBID_TAGS: ['style', 'mstyle'],
ALLOW_UNKNOWN_PROTOCOLS: true,
};
diff --git a/app/assets/javascripts/lib/utils/autosave.js b/app/assets/javascripts/lib/utils/autosave.js
index dac1da743a2..01316be06a2 100644
--- a/app/assets/javascripts/lib/utils/autosave.js
+++ b/app/assets/javascripts/lib/utils/autosave.js
@@ -1,8 +1,27 @@
+import { isString } from 'lodash';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+const normalizeKey = (autosaveKey) => {
+ let normalizedKey;
+
+ if (Array.isArray(autosaveKey) && autosaveKey.every(isString)) {
+ normalizedKey = autosaveKey.join('/');
+ } else if (isString(autosaveKey)) {
+ normalizedKey = autosaveKey;
+ } else {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Invalid autosave key');
+ }
+
+ return `autosave/${normalizedKey}`;
+};
+
+const lockVersionKey = (autosaveKey) => `${normalizeKey(autosaveKey)}/lockVersion`;
+
export const clearDraft = (autosaveKey) => {
try {
- window.localStorage.removeItem(`autosave/${autosaveKey}`);
+ window.localStorage.removeItem(normalizeKey(autosaveKey));
+ window.localStorage.removeItem(lockVersionKey(autosaveKey));
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
@@ -11,7 +30,17 @@ export const clearDraft = (autosaveKey) => {
export const getDraft = (autosaveKey) => {
try {
- return window.localStorage.getItem(`autosave/${autosaveKey}`);
+ return window.localStorage.getItem(normalizeKey(autosaveKey));
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ return null;
+ }
+};
+
+export const getLockVersion = (autosaveKey) => {
+ try {
+ return window.localStorage.getItem(lockVersionKey(autosaveKey));
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
@@ -19,9 +48,12 @@ export const getDraft = (autosaveKey) => {
}
};
-export const updateDraft = (autosaveKey, text) => {
+export const updateDraft = (autosaveKey, text, lockVersion) => {
try {
- window.localStorage.setItem(`autosave/${autosaveKey}`, text);
+ window.localStorage.setItem(normalizeKey(autosaveKey), text);
+ if (lockVersion) {
+ window.localStorage.setItem(lockVersionKey(autosaveKey), lockVersion);
+ }
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 7925a10344a..4448a106bb6 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -60,6 +60,15 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa
});
};
+/**
+ * Return the given element's offset height, or 0 if the element doesn't exist.
+ * Probably not useful outside of handleLocationHash.
+ *
+ * @param {HTMLElement} element The element to measure.
+ * @returns {number} The element's offset height.
+ */
+const getElementOffsetHeight = (element) => element?.offsetHeight ?? 0;
+
// automatically adjust scroll position for hash urls taking the height of the navbar into account
// https://github.com/twitter/bootstrap/issues/1768
export const handleLocationHash = () => {
@@ -84,40 +93,26 @@ export const handleLocationHash = () => {
const fixedIssuableTitle = document.querySelector('.issue-sticky-header');
let adjustment = 0;
- if (fixedNav) adjustment -= fixedNav.offsetHeight;
-
- if (target && target.scrollIntoView) {
- target.scrollIntoView(true);
- }
- if (fixedTabs) {
- adjustment -= fixedTabs.offsetHeight;
- }
-
- if (fixedDiffStats) {
- adjustment -= fixedDiffStats.offsetHeight;
- }
-
- if (performanceBar) {
- adjustment -= performanceBar.offsetHeight;
- }
-
- if (diffFileHeader) {
- adjustment -= diffFileHeader.offsetHeight;
- }
-
- if (versionMenusContainer) {
- adjustment -= versionMenusContainer.offsetHeight;
- }
+ adjustment -= getElementOffsetHeight(fixedNav);
+ adjustment -= getElementOffsetHeight(fixedTabs);
+ adjustment -= getElementOffsetHeight(fixedDiffStats);
+ adjustment -= getElementOffsetHeight(performanceBar);
+ adjustment -= getElementOffsetHeight(diffFileHeader);
+ adjustment -= getElementOffsetHeight(versionMenusContainer);
if (isInIssuePage()) {
- adjustment -= fixedIssuableTitle.offsetHeight;
+ adjustment -= getElementOffsetHeight(fixedIssuableTitle);
}
if (isInMRPage()) {
adjustment -= topPadding;
}
+ if (target?.scrollIntoView) {
+ target.scrollIntoView(true);
+ }
+
setTimeout(() => {
window.scrollBy(0, adjustment);
});
@@ -172,7 +167,7 @@ export const contentTop = () => {
return size;
},
- () => getOuterHeight('.merge-request-tabs'),
+ () => getOuterHeight('.merge-request-sticky-header, .merge-request-tabs'),
() => getOuterHeight('.js-diff-files-changed'),
() => getOuterHeight('.issue-sticky-header.gl-fixed'),
({ desktop }) => {
@@ -180,7 +175,9 @@ export const contentTop = () => {
let size;
if (desktop && diffsTabIsActive) {
- size = getOuterHeight('.diff-file .file-title-flex-parent:not([style="display:none"])');
+ size = getOuterHeight(
+ '.diffs .diff-file .file-title-flex-parent:not([style="display:none"])',
+ );
}
return size;
diff --git a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
index 6c5d4ecc901..c11cf1a7882 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
@@ -271,24 +271,6 @@ export const secondsToMilliseconds = (seconds) => seconds * 1000;
export const secondsToDays = (seconds) => Math.round(seconds / 86400);
/**
- * Converts a numeric utc offset in seconds to +/- hours
- * ie -32400 => -9 hours
- * ie -12600 => -3.5 hours
- *
- * @param {Number} offset UTC offset in seconds as a integer
- *
- * @return {String} the + or - offset in hours
- */
-export const secondsToHours = (offset) => {
- const parsed = parseInt(offset, 10);
- if (Number.isNaN(parsed) || parsed === 0) {
- return `0`;
- }
- const num = offset / 3600;
- return parseInt(num, 10) !== num ? num.toFixed(1) : num;
-};
-
-/**
* Returns the date `n` days after the date provided
*
* @param {Date} date the initial date
diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
index d07abb72210..737c18d1bce 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -406,3 +406,29 @@ export const durationTimeFormatted = (duration) => {
return `${hh}:${mm}:${ss}`;
};
+
+/**
+ * Converts a numeric utc offset in seconds to +/- hours
+ * ie -32400 => -9 hours
+ * ie -12600 => -3.5 hours
+ *
+ * @param {Number} offset UTC offset in seconds as a integer
+ *
+ * @return {String} the + or - offset in hours, e.g. `- 10`, `0`, `+ 4`
+ */
+export const formatUtcOffset = (offset) => {
+ const parsed = parseInt(offset, 10);
+ if (Number.isNaN(parsed) || parsed === 0) {
+ return `0`;
+ }
+ const prefix = offset > 0 ? '+' : '-';
+ return `${prefix} ${Math.abs(offset / 3600)}`;
+};
+
+/**
+ * Returns formatted timezone
+ *
+ * @param {Object} timezone item with offset and name
+ * @returns {String} the UTC timezone with the offset, e.g. `[UTC + 2] Berlin`
+ */
+export const formatTimezone = ({ offset, name }) => `[UTC ${formatUtcOffset(offset)}] ${name}`;
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 48be8af3ff6..3894ec36a0b 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -391,13 +391,15 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo
/**
* Indents selected lines to the right by 2 spaces
*
- * @param {Object} textArea - the targeted text area
+ * @param {Object} textArea - jQuery object with the targeted text area
*/
-function indentLines(textArea) {
+function indentLines($textArea) {
+ const textArea = $textArea.get(0);
const { lines, selectionStart, selectionEnd, startPos, endPos } = linesFromSelection(textArea);
const shiftedLines = [];
let totalAdded = 0;
+ textArea.focus();
textArea.setSelectionRange(startPos, endPos);
lines.forEach((line) => {
@@ -418,13 +420,15 @@ function indentLines(textArea) {
*
* @param {Object} textArea - the targeted text area
*/
-function outdentLines(textArea) {
+function outdentLines($textArea) {
+ const textArea = $textArea.get(0);
const { lines, selectionStart, selectionEnd, startPos, endPos } = linesFromSelection(textArea);
const shiftedLines = [];
let totalRemoved = 0;
let removedFromFirstline = -1;
let removedFromLine = 0;
+ textArea.focus();
textArea.setSelectionRange(startPos, endPos);
lines.forEach((line) => {
@@ -460,28 +464,10 @@ function outdentLines(textArea) {
);
}
-function handleIndentOutdent(e, textArea) {
- if (e.altKey || e.ctrlKey || e.shiftKey) return;
- if (!e.metaKey) return;
-
- switch (e.key) {
- case ']':
- e.preventDefault();
- indentLines(textArea);
- break;
- case '[':
- e.preventDefault();
- outdentLines(textArea);
- break;
- default:
- break;
- }
-}
-
/* eslint-disable @gitlab/require-i18n-strings */
function handleSurroundSelectedText(e, textArea) {
if (!gon.markdown_surround_selection) return;
- if (e.metaKey) return;
+ if (e.metaKey || e.ctrlKey) return;
if (textArea.selectionStart === textArea.selectionEnd) return;
const keys = {
@@ -532,6 +518,7 @@ function continueOlText(listLineMatch, nextLineMatch) {
}
function handleContinueList(e, textArea) {
+ if (!gon.markdown_automatic_lists) return;
if (!(e.key === 'Enter')) return;
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
if (textArea.selectionStart !== textArea.selectionEnd) return;
@@ -586,7 +573,6 @@ export function keypressNoteText(e) {
if ($(textArea).atwho?.('isSelecting')) return;
- handleIndentOutdent(e, textArea);
handleContinueList(e, textArea);
handleSurroundSelectedText(e, textArea);
}
@@ -600,15 +586,26 @@ export function compositionEndNoteText() {
}
export function updateTextForToolbarBtn($toolbarBtn) {
- return updateText({
- textArea: $toolbarBtn.closest('.md-area').find('textarea'),
- tag: $toolbarBtn.data('mdTag'),
- cursorOffset: $toolbarBtn.data('mdCursorOffset'),
- blockTag: $toolbarBtn.data('mdBlock'),
- wrap: !$toolbarBtn.data('mdPrepend'),
- select: $toolbarBtn.data('mdSelect'),
- tagContent: $toolbarBtn.attr('data-md-tag-content'),
- });
+ const $textArea = $toolbarBtn.closest('.md-area').find('textarea');
+
+ switch ($toolbarBtn.data('mdCommand')) {
+ case 'indentLines':
+ indentLines($textArea);
+ break;
+ case 'outdentLines':
+ outdentLines($textArea);
+ break;
+ default:
+ return updateText({
+ textArea: $textArea,
+ tag: $toolbarBtn.data('mdTag'),
+ cursorOffset: $toolbarBtn.data('mdCursorOffset'),
+ blockTag: $toolbarBtn.data('mdBlock'),
+ wrap: !$toolbarBtn.data('mdPrepend'),
+ select: $toolbarBtn.data('mdSelect'),
+ tagContent: $toolbarBtn.attr('data-md-tag-content'),
+ });
+ }
}
export function addMarkdownListeners(form) {
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 59645d50e29..367180714df 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,5 +1,5 @@
import { isString, memoize } from 'lodash';
-
+import { base64ToBuffer, bufferToBase64 } from '~/authentication/webauthn/util';
import {
TRUNCATE_WIDTH_DEFAULT_WIDTH,
TRUNCATE_WIDTH_DEFAULT_FONT_SIZE,
@@ -513,3 +513,15 @@ export const limitedCounterWithDelimiter = (count) => {
return count > limit ? '1,000+' : count;
};
+
+// Encoding UTF8 ⇢ base64
+export function base64EncodeUnicode(str) {
+ const encoder = new TextEncoder('utf8');
+ return bufferToBase64(encoder.encode(str));
+}
+
+// Decoding base64 ⇢ UTF8
+export function base64DecodeUnicode(str) {
+ const decoder = new TextDecoder('utf8');
+ return decoder.decode(base64ToBuffer(str));
+}
diff --git a/app/assets/javascripts/listbox/index.js b/app/assets/javascripts/listbox/index.js
index f63171e2785..7eacbf7fcdd 100644
--- a/app/assets/javascripts/listbox/index.js
+++ b/app/assets/javascripts/listbox/index.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlListbox } from '@gitlab/ui';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
@@ -31,37 +31,25 @@ export function initListbox(el, { onChange } = {}) {
},
},
render(h) {
- return h(
- GlDropdown,
- {
- props: {
- text: this.text,
- right,
+ return h(GlListbox, {
+ props: {
+ items,
+ right,
+ selected: this.selected,
+ toggleText: this.text,
+ },
+ class: className,
+ on: {
+ select: (selectedValue) => {
+ this.selected = selectedValue;
+ const selectedItem = items.find(({ value }) => value === selectedValue);
+
+ if (typeof onChange === 'function') {
+ onChange(selectedItem);
+ }
},
- class: className,
},
- items.map((item) =>
- h(
- GlDropdownItem,
- {
- props: {
- isCheckItem: true,
- isChecked: this.selected === item.value,
- },
- on: {
- click: () => {
- this.selected = item.value;
-
- if (typeof onChange === 'function') {
- onChange(item);
- }
- },
- },
- },
- item.text,
- ),
- ),
- );
+ });
},
});
}
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index c570f8810a8..ca3f1caec67 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,5 +1,5 @@
export default function initLogoAnimation() {
window.addEventListener('beforeunload', () => {
- document.querySelector('.tanuki-logo').classList.add('animate');
+ document.querySelector('.tanuki-logo')?.classList.add('animate');
});
}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index c16ed68096d..8e4ebd510aa 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -21,7 +21,7 @@ import initTodoToggle from './header';
import initLayoutNav from './layout_nav';
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime/timeago_utility';
-import { getLocationHash, visitUrl } from './lib/utils/url_utility';
+import { getLocationHash, visitUrl, mergeUrlParams } from './lib/utils/url_utility';
// everything else
import initFeatureHighlight from './feature_highlight';
@@ -250,11 +250,10 @@ $('form.filter-form').on('submit', function filterFormSubmitCallback(event) {
const link = document.createElement('a');
link.href = this.action;
- const action = `${this.action}${link.search === '' ? '?' : '&'}`;
+ const action = mergeUrlParams(Object.fromEntries(new FormData(this)), this.action);
event.preventDefault();
- // eslint-disable-next-line no-jquery/no-serialize
- visitUrl(`${action}${$(this).serialize()}`);
+ visitUrl(action);
});
const flashContainer = document.querySelector('.flash-container');
diff --git a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
index ce28283ccdf..01f145e0862 100644
--- a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
+++ b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
@@ -4,6 +4,7 @@ import { mapState } from 'vuex';
import { visitUrl } from '~/lib/utils/url_utility';
import { FIELDS } from '~/members/constants';
import { parseSortParam, buildSortHref } from '~/members/utils';
+import { SORT_DIRECTION_UI } from '~/search/sort/constants';
export default {
name: 'SortDropdown',
@@ -30,6 +31,9 @@ export default {
isAscending() {
return !this.sort.sortDesc;
},
+ sortDirectionData() {
+ return this.isAscending ? SORT_DIRECTION_UI.asc : SORT_DIRECTION_UI.desc;
+ },
filteredOptions() {
return FIELDS.filter(
(field) => this.tableSortableFields.includes(field.key) && field.sort,
@@ -70,7 +74,7 @@ export default {
data-testid="members-sort-dropdown"
:text="activeOptionLabel"
:is-ascending="isAscending"
- :sort-direction-tool-tip="__('Sort direction')"
+ :sort-direction-tool-tip="sortDirectionData.tooltip"
@sortDirectionChange="handleSortDirectionChange"
>
<gl-sorting-item
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 460dc0041ab..0512bc04085 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -2,7 +2,7 @@
import { GlTable, GlBadge, GlPagination } from '@gitlab/ui';
import { mapState } from 'vuex';
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
-import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
+import { canUnban, canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import UserDate from '~/vue_shared/components/user_date.vue';
import {
@@ -90,7 +90,8 @@ export default {
canRemove(member) ||
canResend(member) ||
canUpdate(member, this.currentUserId) ||
- canOverride(member)
+ canOverride(member) ||
+ canUnban(member)
);
},
showField(field) {
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index 0da44b7d468..bf87ab53d36 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -105,9 +105,12 @@ export const buildSortHref = ({
return setUrlParams({ ...filterParams, sort: sortParam }, window.location.href, true);
};
-// Defined in `ee/app/assets/javascripts/vue_shared/components/members/utils.js`
+// Defined in `ee/app/assets/javascripts/members/utils.js`
export const canOverride = () => false;
+// Defined in `ee/app/assets/javascripts/members/utils.js`
+export const canUnban = () => false;
+
export const parseDataAttributes = (el) => {
const { membersData } = el.dataset;
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
index 7168efa28ad..707e8a0645f 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
@@ -2,7 +2,7 @@
import { GlButton } from '@gitlab/ui';
import { debounce } from 'lodash';
import { mapActions } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { INTERACTIVE_RESOLVE_MODE } from '../constants';
@@ -75,7 +75,7 @@ export default {
},
)
.catch(() => {
- createFlash({
+ createAlert({
message: __('An error occurred while loading the file'),
});
});
diff --git a/app/assets/javascripts/merge_conflicts/store/actions.js b/app/assets/javascripts/merge_conflicts/store/actions.js
index 9c101da52f5..f84eaabf9e7 100644
--- a/app/assets/javascripts/merge_conflicts/store/actions.js
+++ b/app/assets/javascripts/merge_conflicts/store/actions.js
@@ -1,5 +1,5 @@
import { setCookie } from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { INTERACTIVE_RESOLVE_MODE, EDIT_RESOLVE_MODE } from '../constants';
@@ -33,7 +33,7 @@ export const submitResolvedConflicts = async ({ commit, getters }, resolveConfli
window.location.assign(data.redirect_to);
} catch (e) {
commit(types.SET_SUBMIT_STATE, false);
- createFlash({ message: __('Failed to save merge conflicts resolutions. Please try again!') });
+ createAlert({ message: __('Failed to save merge conflicts resolutions. Please try again!') });
}
};
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 8cdb9eb5fc4..57b5e9809d2 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, no-underscore-dangle, consistent-return */
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -44,7 +44,7 @@ function MergeRequest(opts) {
}
},
onError: () => {
- createFlash({
+ createAlert({
message: __(
'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
),
@@ -98,7 +98,7 @@ MergeRequest.prototype.initMRBtnListeners = function () {
MergeRequest.toggleDraftStatus(data.title, wipEvent === 'ready');
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
})
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 0b53a8ede64..17ee2a0d8b6 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,12 +1,12 @@
/* eslint-disable no-new, class-methods-use-this */
import $ from 'jquery';
import Vue from 'vue';
+import { createAlert } from '~/flash';
import { getCookie, isMetaClick, parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
import { parseUrlPathname } from '~/lib/utils/url_utility';
import createEventHub from '~/helpers/event_hub_factory';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import Diff from './diff';
-import createFlash from './flash';
import { initDiffStatsDropdown } from './init_diff_stats_dropdown';
import axios from './lib/utils/axios_utils';
@@ -447,7 +447,7 @@ export default class MergeRequestTabs {
.then((m) => m.default())
.catch(() => {
toggleLoader(false);
- createFlash({
+ createAlert({
message: __('An error occurred while fetching this tab.'),
});
});
@@ -480,7 +480,7 @@ export default class MergeRequestTabs {
this.diffsLoaded = true;
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('An error occurred while fetching this tab.'),
});
})
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index f067982fce1..b7629ba001f 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -86,6 +86,7 @@ export default {
<template>
<gl-intersection-observer
+ class="gl-relative gl-top-2"
@appear="setStickyHeaderVisible(false)"
@disappear="setStickyHeaderVisible(true)"
>
diff --git a/app/assets/javascripts/milestones/components/promote_milestone_modal.vue b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
index cac6d722ced..9e537fa2c82 100644
--- a/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
@@ -63,7 +63,7 @@ export default {
visitUrl(response.data.url);
})
.catch((error) => {
- createFlash({
+ createAlert({
message: error,
});
})
diff --git a/app/assets/javascripts/milestones/milestone.js b/app/assets/javascripts/milestones/milestone.js
index 8f2721c2a5b..d9e72340d62 100644
--- a/app/assets/javascripts/milestones/milestone.js
+++ b/app/assets/javascripts/milestones/milestone.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -34,7 +34,7 @@ export default class Milestone {
this.loadedTabs.add(tab);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error loading milestone tab'),
}),
);
diff --git a/app/assets/javascripts/milestones/milestone_select.js b/app/assets/javascripts/milestones/milestone_select.js
index c95ec3dd10b..d4876c3dbe8 100644
--- a/app/assets/javascripts/milestones/milestone_select.js
+++ b/app/assets/javascripts/milestones/milestone_select.js
@@ -121,7 +121,7 @@ export default class MilestoneSelect {
title: __('Started'),
});
}
- if (extraOptions.length) {
+ if (extraOptions.length && data.length) {
extraOptions.push({ type: 'divider' });
}
diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js
index 5bf08be1ead..2995f19c470 100644
--- a/app/assets/javascripts/mirrors/mirror_repos.js
+++ b/app/assets/javascripts/mirrors/mirror_repos.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { hide } from '~/tooltips';
@@ -120,7 +120,7 @@ export default class MirrorRepos {
.put(this.mirrorEndpoint, payload)
.then(() => this.removeRow($target))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Failed to remove mirror.'),
}),
);
diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js
index eb7c43034a4..3b7e5a5f2ee 100644
--- a/app/assets/javascripts/mirrors/ssh_mirror.js
+++ b/app/assets/javascripts/mirrors/ssh_mirror.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { escape } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -115,7 +115,7 @@ export default class SSHMirror {
const failureMessage = response.data
? response.data.message
: __('An error occurred while detecting host keys');
- createFlash({
+ createAlert({
message: failureMessage,
});
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index e3fcdf716d4..b6ad2d21757 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -11,7 +11,7 @@ import {
import Mousetrap from 'mousetrap';
import VueDraggable from 'vuedraggable';
import { mapActions, mapState, mapGetters } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import invalidUrl from '~/lib/utils/invalid_url';
import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -176,7 +176,7 @@ export default {
this.setExpandedPanel(expandedPanel);
}
} catch {
- createFlash({
+ createAlert({
message: s__(
'Metrics|Link contains invalid chart information, please verify the link to see the expanded panel.',
),
@@ -201,7 +201,7 @@ export default {
* This watcher is set for future SPA behaviour of the dashboard
*/
if (hasWarnings) {
- createFlash({
+ createAlert({
message: s__(
'Metrics|Your dashboard schema is invalid. Edit the dashboard to correct the YAML schema.',
),
@@ -319,7 +319,7 @@ export default {
this.isRearrangingPanels = isRearrangingPanels;
},
onDateTimePickerInvalid() {
- createFlash({
+ createAlert({
message: s__(
'Metrics|Link contains an invalid time window, please verify the link to see the requested time range.',
),
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 5c99dbc0d98..0ef365c6368 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/browser';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
@@ -134,7 +134,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
if (state.showErrorBanner) {
if (error.response.data && error.response.data.message) {
const { message } = error.response.data;
- createFlash({
+ createAlert({
message: sprintf(
s__('Metrics|There was an error while retrieving metrics. %{message}'),
{ message },
@@ -142,7 +142,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
),
});
} else {
- createFlash({
+ createAlert({
message: s__('Metrics|There was an error while retrieving metrics'),
});
}
@@ -176,7 +176,7 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
dispatch('fetchDeploymentsData');
if (!state.timeRange) {
- createFlash({
+ createAlert({
message: s__(`Metrics|Invalid time range, please verify.`),
type: 'warning',
});
@@ -207,7 +207,7 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
});
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__(`Metrics|There was an error while retrieving metrics`),
type: 'warning',
});
@@ -246,7 +246,7 @@ export const fetchPrometheusMetric = (
Sentry.captureException(error);
commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metricId, error });
- // Continue to throw error so the dashboard can notify using createFlash
+ // Continue to throw error so the dashboard can notify using createAlert
throw error;
});
};
@@ -262,7 +262,7 @@ export const fetchDeploymentsData = ({ state, dispatch }) => {
.then((resp) => resp.data)
.then((response) => {
if (!response || !response.deployments) {
- createFlash({
+ createAlert({
message: s__('Metrics|Unexpected deployment data response from prometheus endpoint'),
});
}
@@ -272,7 +272,7 @@ export const fetchDeploymentsData = ({ state, dispatch }) => {
.catch((error) => {
Sentry.captureException(error);
dispatch('receiveDeploymentsDataFailure');
- createFlash({
+ createAlert({
message: s__('Metrics|There was an error getting deployment information.'),
});
});
@@ -302,7 +302,7 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
)
.then((environments) => {
if (!environments) {
- createFlash({
+ createAlert({
message: s__(
'Metrics|There was an error fetching the environments data, please try again',
),
@@ -314,7 +314,7 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveEnvironmentsDataFailure');
- createFlash({
+ createAlert({
message: s__('Metrics|There was an error getting environments information.'),
});
});
@@ -348,7 +348,7 @@ export const fetchAnnotations = ({ state, dispatch, getters }) => {
.then(parseAnnotationsResponse)
.then((annotations) => {
if (!annotations) {
- createFlash({
+ createAlert({
message: s__('Metrics|There was an error fetching annotations. Please try again.'),
});
}
@@ -358,7 +358,7 @@ export const fetchAnnotations = ({ state, dispatch, getters }) => {
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveAnnotationsFailure');
- createFlash({
+ createAlert({
message: s__('Metrics|There was an error getting annotations information.'),
});
});
@@ -397,7 +397,7 @@ export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) =
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveDashboardValidationWarningsFailure');
- createFlash({
+ createAlert({
message: s__(
'Metrics|There was an error getting dashboard validation warnings information.',
),
@@ -502,7 +502,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data });
})
.catch(() => {
- createFlash({
+ createAlert({
message: sprintf(
s__('Metrics|There was an error getting options for variable "%{name}".'),
{
@@ -569,7 +569,7 @@ export const fetchPanelPreviewMetrics = ({ state, commit }) => {
Sentry.captureException(error);
commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE, { index, error });
- // Continue to throw error so the panel builder can notify using createFlash
+ // Continue to throw error so the panel builder can notify using createAlert
throw error;
});
});
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index 297420bf94d..c32a1f4c2ac 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -6,7 +6,6 @@ import initDiffsApp from '../diffs';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import MergeRequest from '../merge_request';
import DiscussionCounter from '../notes/components/discussion_counter.vue';
-import initDiscussionFilters from '../notes/discussion_filters';
import initNotesApp from './init_notes';
export default function initMrNotes() {
@@ -49,7 +48,5 @@ export default function initMrNotes() {
},
});
}
-
- initDiscussionFilters(store);
});
}
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index e4a7a7bd9fc..3a67e7925c3 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -5,19 +5,27 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import store from '~/mr_notes/stores';
import discussionNavigator from '../notes/components/discussion_navigator.vue';
import NotesApp from '../notes/components/notes_app.vue';
+import { getNotesFilterData } from '../notes/utils/get_notes_filter_data';
import initWidget from '../vue_merge_request_widget';
export default () => {
+ const el = document.getElementById('js-vue-mr-discussions');
+ if (!el) {
+ return;
+ }
+
+ const notesFilterProps = getNotesFilterData(el);
+
// eslint-disable-next-line no-new
new Vue({
- el: '#js-vue-mr-discussions',
+ el,
name: 'MergeRequestDiscussions',
components: {
NotesApp,
},
store,
data() {
- const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
+ const notesDataset = el.dataset;
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
noteableData.targetType = notesDataset.targetType;
@@ -95,6 +103,7 @@ export default () => {
userData: this.currentUserData,
shouldShow: this.isShowTabActive,
helpPagePath: this.helpPagePath,
+ ...notesFilterProps,
},
}),
]);
diff --git a/app/assets/javascripts/namespaces/leave_by_url.js b/app/assets/javascripts/namespaces/leave_by_url.js
index e00c2abfbef..09757ce17fa 100644
--- a/app/assets/javascripts/namespaces/leave_by_url.js
+++ b/app/assets/javascripts/namespaces/leave_by_url.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { initRails } from '~/lib/utils/rails_ujs';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
@@ -18,7 +18,7 @@ export default function leaveByUrl(namespaceType) {
if (leaveLink) {
leaveLink.click();
} else {
- createFlash({
+ createAlert({
message: sprintf(__('You do not have permission to leave this %{namespaceType}.'), {
namespaceType,
}),
diff --git a/app/assets/javascripts/nav/components/top_nav_app.vue b/app/assets/javascripts/nav/components/top_nav_app.vue
index ca6e6567f74..e55bf25a60c 100644
--- a/app/assets/javascripts/nav/components/top_nav_app.vue
+++ b/app/assets/javascripts/nav/components/top_nav_app.vue
@@ -1,5 +1,6 @@
<script>
import { GlNav, GlIcon, GlNavItemDropdown, GlDropdownForm, GlTooltipDirective } from '@gitlab/ui';
+import Tracking from '~/tracking';
import TopNavDropdownMenu from './top_nav_dropdown_menu.vue';
export default {
@@ -19,6 +20,14 @@ export default {
required: true,
},
},
+ methods: {
+ trackToggleEvent() {
+ Tracking.event(undefined, 'click_nav', {
+ label: 'hamburger_menu',
+ property: 'top_navigation',
+ });
+ },
+ },
};
</script>
@@ -32,6 +41,7 @@ export default {
toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
no-flip
no-caret
+ @toggle="trackToggleEvent"
>
<template #button-content>
<gl-icon name="hamburger" />
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index fdcea300388..5437a607e8a 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -44,7 +44,7 @@ export default {
sandbox
:srcdoc="rawCode"
frameborder="0"
- scrolling="no"
+ scrolling="auto"
width="100%"
class="gl-overflow-auto"
></iframe>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index bf35d5c3b25..0d7ff022f8f 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -5,7 +5,7 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { badgeState } from '~/issuable/components/status_box.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import {
@@ -111,7 +111,10 @@ export default {
return this.getNoteableData.current_user.can_create_note;
},
canSetInternalNote() {
- return this.getNoteableData.current_user.can_update && (this.isIssue || this.isEpic);
+ return (
+ this.getNoteableData.current_user.can_create_confidential_note &&
+ (this.isIssue || this.isEpic)
+ );
},
issueActionButtonTitle() {
const openOrClose = this.isOpen ? 'close' : 'reopen';
@@ -276,7 +279,7 @@ export default {
.then(() => badgeState.updateStatus && badgeState.updateStatus())
.then(refreshUserMergeRequestCounts)
.catch(() =>
- createFlash({
+ createAlert({
message: constants.toggleStateErrorMessage[this.noteableType][this.openState],
}),
);
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index 1b1923a90f7..cf6474270a2 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -84,8 +84,8 @@ export default {
return sprintf(text, { commitDisplay, linkStart, linkEnd }, false);
},
- adaptiveAvatarSize() {
- return { default: 24, md: 32 };
+ toggleClass() {
+ return this.discussion.expanded ? 'expanded' : 'collapsed';
},
},
methods: {
@@ -98,16 +98,13 @@ export default {
</script>
<template>
- <div class="discussion-header gl-display-flex gl-align-items-center gl-p-5">
- <div
- v-once
- class="timeline-icon gl-align-self-start gl-flex-shrink-0 gl-flex-shrink gl-mx-3 gl-md-ml-2 gl-md-mr-5"
- >
+ <div class="discussion-header gl-display-flex gl-align-items-center">
+ <div v-once class="timeline-avatar gl-align-self-start gl-flex-shrink-0 gl-flex-shrink">
<gl-avatar-link v-if="author" :href="author.path">
- <gl-avatar :src="author.avatar_url" :alt="author.name" :size="adaptiveAvatarSize" />
+ <gl-avatar :src="author.avatar_url" :alt="author.name" :size="32" />
</gl-avatar-link>
</div>
- <div class="timeline-content w-100">
+ <div class="timeline-content w-100 gl-ml-3" :class="toggleClass">
<note-header
:author="author"
:created-at="firstNote.created_at"
@@ -123,14 +120,14 @@ export default {
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline gl-pl-2"
+ class-name="discussion-headline-light js-discussion-headline gl-pl-3"
/>
<note-edited-text
v-else-if="lastUpdatedAt"
:edited-at="lastUpdatedAt"
:edited-by="lastUpdatedBy"
:action-text="__('Last updated')"
- class-name="discussion-headline-light js-discussion-headline gl-pl-2"
+ class-name="discussion-headline-light js-discussion-headline gl-pl-3"
/>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 6521b86edbb..37935e9c3c6 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -81,16 +81,18 @@ export default {
:class="{
'gl-bg-orange-50': blocksMerge && !allResolved,
'gl-bg-gray-50': !blocksMerge || allResolved,
- 'gl-pr-2': !allResolved,
}"
data-testid="discussions-counter-text"
>
<template v-if="allResolved">
{{ __('All threads resolved!') }}
<gl-dropdown
+ v-gl-tooltip:discussionCounter.hover.bottom
size="small"
category="tertiary"
right
+ :title="__('Thread options')"
+ :aria-label="__('Thread options')"
toggle-class="btn-icon"
class="gl-pt-0! gl-px-2 gl-h-full gl-ml-2"
>
@@ -133,9 +135,12 @@ export default {
@click="jumpNext"
/>
<gl-dropdown
+ v-gl-tooltip:discussionCounter.hover.bottom
size="small"
category="tertiary"
right
+ :title="__('Thread options')"
+ :aria-label="__('Thread options')"
toggle-class="btn-icon"
class="gl-pt-0! gl-px-2"
>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 8a42fb6bd85..21b48a2a666 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -168,7 +168,7 @@ export default {
id="discussion-preferences-dropdown"
class="full-width-mobile"
data-qa-selector="discussion_preferences_dropdown"
- text="Sort or filter"
+ :text="__('Sort or filter')"
:disabled="isLoading"
right
>
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
index 61af0b06535..39b3df899a5 100644
--- a/app/assets/javascripts/notes/components/discussion_filter_note.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -31,7 +31,7 @@ export default {
<div class="timeline-icon d-none d-lg-flex">
<gl-icon name="comment" />
</div>
- <div class="timeline-content">
+ <div class="timeline-content gl-pl-8">
<div data-testid="discussion-filter-timeline-content">
<gl-sprintf :message="$options.i18n.information">
<template #bold="{ content }">
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 6fcfa66ea49..2dbc9b10836 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -142,7 +142,7 @@ export default {
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text gl-mb-2 gl-ml-3"
/>
</template>
<template #avatar-badge>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 9806f8e5dc2..930876e90b1 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui
import { mapActions, mapGetters, mapState } from 'vuex';
import Api from '~/api';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import eventHub from '~/sidebar/event_hub';
@@ -238,7 +238,7 @@ export default {
})
.then(() => this.handleAssigneeUpdate(assignees))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong while updating assignees'),
}),
);
@@ -281,6 +281,7 @@ export default {
>
{{ __('Contributor') }}
</user-access-role-badge>
+ <span class="note-actions__mobile-spacer"></span>
<gl-button
v-if="canResolve"
ref="resolveButton"
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 835750cc137..9d59994788e 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import AwardsList from '~/vue_shared/components/awards_list.vue';
@@ -49,7 +49,7 @@ export default {
};
this.toggleAwardRequest(data).catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong on our end.'),
}),
);
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index f700802d6bc..f3530344181 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -9,8 +9,6 @@ import {
import { mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
@@ -21,13 +19,11 @@ export default {
GlIcon,
GlBadge,
GlLoadingIcon,
- UserNameWithStatus,
},
directives: {
SafeHtml,
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagsMixin()],
props: {
author: {
type: Object,
@@ -74,12 +70,15 @@ export default {
required: false,
default: false,
},
+ isSystemNote: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
isUsernameLinkHovered: false,
- emojiTitle: '',
- authorStatusHasTooltip: false,
};
},
computed: {
@@ -100,15 +99,6 @@ export default {
'js-user-link': true,
};
},
- authorStatus() {
- if (this.author?.show_status) {
- return this.author.status_tooltip_html;
- }
- return false;
- },
- emojiElement() {
- return this.$refs?.authorStatus?.querySelector('gl-emoji');
- },
authorName() {
return this.author.name;
},
@@ -116,14 +106,6 @@ export default {
return s__('Notes|This internal note will always remain confidential');
},
},
- mounted() {
- this.emojiTitle = this.emojiElement ? this.emojiElement.getAttribute('title') : '';
-
- const authorStatusTitle = this.$refs?.authorStatus
- ?.querySelector('.user-status-emoji')
- ?.getAttribute('title');
- this.authorStatusHasTooltip = authorStatusTitle && authorStatusTitle !== '';
- },
methods: {
...mapActions(['setTargetNoteHash']),
handleToggle() {
@@ -134,12 +116,6 @@ export default {
this.setTargetNoteHash(this.noteTimestampLink);
}
},
- removeEmojiTitle() {
- this.emojiElement.removeAttribute('title');
- },
- addEmojiTitle() {
- this.emojiElement.setAttribute('title', this.emojiTitle);
- },
handleUsernameMouseEnter() {
this.$refs.authorNameLink.dispatchEvent(new Event('mouseenter'));
this.isUsernameLinkHovered = true;
@@ -148,9 +124,6 @@ export default {
this.$refs.authorNameLink.dispatchEvent(new Event('mouseleave'));
this.isUsernameLinkHovered = false;
},
- userAvailability(selectedAuthor) {
- return selectedAuthor?.availability || '';
- },
},
i18n: {
showThread: __('Show thread'),
@@ -185,35 +158,11 @@ export default {
:data-user-id="author.id"
:data-username="author.username"
>
- <span
- v-if="glFeatures.removeUserAttributesProjects || glFeatures.removeUserAttributesGroups"
- class="note-header-author-name gl-font-weight-bold"
- >
+ <span class="note-header-author-name gl-font-weight-bold">
{{ authorName }}
</span>
- <user-name-with-status
- v-else
- :name="authorName"
- :availability="userAvailability(author)"
- container-classes="note-header-author-name gl-font-weight-bold"
- />
</a>
- <span
- v-if="
- authorStatus &&
- !glFeatures.removeUserAttributesProjects &&
- !glFeatures.removeUserAttributesGroups
- "
- ref="authorStatus"
- v-safe-html:[$options.safeHtmlConfig]="authorStatus"
- v-on="
- authorStatusHasTooltip ? { mouseenter: removeEmojiTitle, mouseleave: addEmojiTitle } : {}
- "
- ></span>
- <span
- v-if="!glFeatures.removeUserAttributesProjects && !glFeatures.removeUserAttributesGroups"
- class="text-nowrap author-username"
- >
+ <span v-if="!isSystemNote" class="text-nowrap author-username">
<a
ref="authorUsernameLink"
class="author-username-link"
@@ -252,7 +201,7 @@ export default {
data-testid="internalNoteIndicator"
variant="warning"
size="sm"
- class="gl-mb-3 gl-ml-2"
+ class="gl-ml-2"
:title="internalNoteTooltip"
>
{{ __('Internal note') }}
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index afa5e39d8b0..50d166b6db5 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import DraftNote from '~/batch_comments/components/draft_note.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@@ -247,7 +247,7 @@ export default {
const msg = __(
'Your comment could not be submitted! Please check your network connection and try again.',
);
- createFlash({
+ createAlert({
message: msg,
parent: this.$el,
});
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index e51969f95c7..c4b3111b919 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -5,7 +5,7 @@ import { escape, isEmpty } from 'lodash';
import { mapGetters, mapActions } from 'vuex';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import { truncateSha } from '~/lib/utils/text_utility';
@@ -199,9 +199,6 @@ export default {
isMRDiffView() {
return this.line && !this.isOverviewTab;
},
- authorAvatarAdaptiveSize() {
- return { default: 24, md: 32 };
- },
},
created() {
const line = this.note.position?.line_range?.start || this.line;
@@ -273,7 +270,7 @@ export default {
this.isDeleting = false;
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong while deleting your note. Please try again.'),
});
this.isDeleting = false;
@@ -352,7 +349,7 @@ export default {
},
handleUpdateError() {
const msg = __('Something went wrong while editing your comment. Please try again.');
- createFlash({
+ createAlert({
message: msg,
parent: this.$el,
});
@@ -409,13 +406,13 @@ export default {
:class="{ ...classNameBindings, 'internal-note': note.internal }"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note note-wrapper"
+ class="note note-wrapper note-comment"
data-qa-selector="noteable_note_container"
>
<div
v-if="showMultiLineComment"
data-testid="multiline-comment"
- class="gl-mb-5 gl-text-gray-500 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-pb-4"
+ class="gl-text-gray-500 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-px-5 gl-py-3"
>
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
<template #startLine>
@@ -427,7 +424,7 @@ export default {
</gl-sprintf>
</div>
- <div v-if="isMRDiffView" class="gl-float-left gl-mt-n1 gl-mr-3">
+ <div v-if="isMRDiffView" class="timeline-avatar gl-float-left gl-pt-2">
<gl-avatar-link :href="author.path">
<gl-avatar
:src="author.avatar_url"
@@ -440,13 +437,13 @@ export default {
</gl-avatar-link>
</div>
- <div v-else class="gl-float-left gl-pl-3 gl-md-pl-2">
+ <div v-else class="timeline-avatar gl-float-left">
<gl-avatar-link :href="author.path">
<gl-avatar
:src="author.avatar_url"
:entity-name="author.username"
:alt="author.name"
- :size="authorAvatarAdaptiveSize"
+ :size="32"
/>
<slot name="avatar-badge"></slot>
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
new file mode 100644
index 00000000000..e4f88962731
--- /dev/null
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -0,0 +1,38 @@
+<script>
+import DiscussionFilter from './discussion_filter.vue';
+
+export default {
+ components: {
+ TimelineToggle: () => import('./timeline_toggle.vue'),
+ DiscussionFilter,
+ },
+ inject: {
+ showTimelineViewToggle: {
+ default: false,
+ },
+ },
+ props: {
+ notesFilters: {
+ type: Array,
+ required: true,
+ },
+ notesFilterValue: {
+ type: Number,
+ default: undefined,
+ required: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-display-flex gl-sm-align-items-center gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-pt-5 gl-mt-5 gl-border-t"
+ >
+ <h2 class="gl-font-size-h1 gl-m-0">{{ __('Activity') }}</h2>
+ <div class="gl-display-flex gl-gap-3 gl-w-full gl-sm-w-auto gl-mt-3 gl-sm-mt-0">
+ <timeline-toggle v-if="showTimelineViewToggle" />
+ <discussion-filter :filters="notesFilters" :selected-value="notesFilterValue" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 37bc8bad305..9c2ff2c3e7f 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,7 +1,7 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
@@ -19,10 +19,12 @@ import DiscussionFilterNote from './discussion_filter_note.vue';
import NoteableDiscussion from './noteable_discussion.vue';
import NoteableNote from './noteable_note.vue';
import SidebarSubscription from './sidebar_subscription.vue';
+import NotesActivityHeader from './notes_activity_header.vue';
export default {
name: 'NotesApp',
components: {
+ NotesActivityHeader,
NoteableNote,
NoteableDiscussion,
SystemNote,
@@ -46,6 +48,15 @@ export default {
type: Object,
required: true,
},
+ notesFilters: {
+ type: Array,
+ required: true,
+ },
+ notesFilterValue: {
+ type: Number,
+ default: undefined,
+ required: false,
+ },
userData: {
type: Object,
required: false,
@@ -221,7 +232,7 @@ export default {
.catch(() => {
this.setLoadingState(false);
this.setNotesFetchedState(true);
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching comments. Please try again.'),
});
});
@@ -281,6 +292,7 @@ export default {
<template>
<div v-show="shouldShow" id="notes">
<sidebar-subscription :iid="noteableData.iid" :noteable-data="noteableData" />
+ <notes-activity-header :notes-filters="notesFilters" :notes-filter-value="notesFilterValue" />
<ordered-layout :slot-keys="slotKeys">
<template #form>
<comment-form
@@ -292,7 +304,11 @@ export default {
<template #comments>
<ul id="notes-list" class="notes main-notes-list timeline">
<template v-for="discussion in allDiscussions">
- <skeleton-loading-container v-if="discussion.isSkeletonNote" :key="discussion.id" />
+ <skeleton-loading-container
+ v-if="discussion.isSkeletonNote"
+ :key="discussion.id"
+ class="note-skeleton"
+ />
<timeline-entry-item v-else-if="discussion.isDraft" :key="discussion.id">
<draft-note :draft="discussion" />
</timeline-entry-item>
@@ -327,7 +343,7 @@ export default {
:help-page-path="helpPagePath"
/>
</template>
- <discussion-filter-note v-show="commentsDisabled" />
+ <discussion-filter-note v-if="commentsDisabled" />
</ul>
</template>
</ordered-layout>
diff --git a/app/assets/javascripts/notes/components/timeline_toggle.vue b/app/assets/javascripts/notes/components/timeline_toggle.vue
index 8632eea5d8e..59a3cc2d306 100644
--- a/app/assets/javascripts/notes/components/timeline_toggle.vue
+++ b/app/assets/javascripts/notes/components/timeline_toggle.vue
@@ -53,6 +53,7 @@ export default {
:selected="timelineEnabled"
:title="tooltip"
:aria-label="tooltip"
+ data-testid="timeline-toggle-button"
@click="toggleTimeline"
/>
</template>
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index 2bd3488ae1b..734e08dd586 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -61,7 +61,7 @@ export default {
<template>
<li
:class="liClasses"
- class="gl-display-flex! gl-align-items-center gl-flex-wrap gl-bg-gray-10 gl-py-3 gl-px-5 gl-border-t"
+ class="toggle-replies-widget gl-display-flex! gl-align-items-center gl-flex-wrap gl-bg-gray-10 gl-py-3 gl-px-5 gl-border"
>
<gl-button
ref="toggle"
@@ -75,7 +75,7 @@ export default {
<user-avatar-link
v-for="author in uniqueAuthors"
:key="author.username"
- class="gl-mr-3"
+ class="gl-mr-3 reply-author-avatar"
:link-href="author.path"
:img-alt="author.name"
img-css-classes="gl-mr-0!"
diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js
deleted file mode 100644
index 104e9d4183a..00000000000
--- a/app/assets/javascripts/notes/discussion_filters.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import Vue from 'vue';
-import DiscussionFilter from './components/discussion_filter.vue';
-
-export default (store) => {
- const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
-
- if (discussionFilterEl) {
- const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
- const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
- const filters = Object.keys(filterValues).map((entry) => ({
- title: entry,
- value: filterValues[entry],
- }));
- const props = { filters };
-
- if (defaultFilter) {
- props.selectedValue = parseInt(defaultFilter, 10);
- }
-
- return new Vue({
- el: discussionFilterEl,
- name: 'DiscussionFilterRoot',
- components: {
- DiscussionFilter,
- },
- store,
- render(createElement) {
- return createElement('discussion-filter', { props });
- },
- });
- }
-
- return null;
-};
diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js
index 08792fd1a3f..9b5fd69f816 100644
--- a/app/assets/javascripts/notes/i18n.js
+++ b/app/assets/javascripts/notes/i18n.js
@@ -16,7 +16,7 @@ export const COMMENT_FORM = {
bodyPlaceholderInternal: __('Write an internal note or drag your files here…'),
internal: s__('Notes|Make this an internal note'),
internalVisibility: s__(
- 'Notes|Internal notes are only visible to the author, assignees, and members with the role of Reporter or higher',
+ 'Notes|Internal notes are only visible to members with the role of Reporter or higher',
),
discussionThatNeedsResolution: __(
'Discuss a specific suggestion or question that needs to be resolved.',
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 054a5bd36e2..defcb0533b7 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,9 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import NotesApp from './components/notes_app.vue';
-import initDiscussionFilters from './discussion_filters';
import { store } from './stores';
-import initTimelineToggle from './timeline';
+import { getNotesFilterData } from './utils/get_notes_filter_data';
export default () => {
const el = document.getElementById('js-vue-notes');
@@ -11,6 +10,9 @@ export default () => {
return;
}
+ const notesFilterProps = getNotesFilterData(el);
+ const showTimelineViewToggle = parseBoolean(el.dataset.showTimelineViewToggle);
+
// eslint-disable-next-line no-new
new Vue({
el,
@@ -19,6 +21,9 @@ export default () => {
NotesApp,
},
store,
+ provide: {
+ showTimelineViewToggle,
+ },
data() {
const notesDataset = el.dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
@@ -56,11 +61,9 @@ export default () => {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
+ ...notesFilterProps,
},
});
},
});
-
- initDiscussionFilters(store);
- initTimelineToggle(store);
};
diff --git a/app/assets/javascripts/notes/mixins/diff_line_note_form.js b/app/assets/javascripts/notes/mixins/diff_line_note_form.js
index 7b9c0959464..9a140029c07 100644
--- a/app/assets/javascripts/notes/mixins/diff_line_note_form.js
+++ b/app/assets/javascripts/notes/mixins/diff_line_note_form.js
@@ -1,7 +1,7 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { getDraftReplyFormData, getDraftFormData } from '~/batch_comments/utils';
import { TEXT_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { clearDraft } from '~/lib/utils/autosave';
import { s__ } from '~/locale';
import { formatLineRange } from '~/notes/components/multiline_comment_utils';
@@ -42,7 +42,7 @@ export default {
this.handleClearForm(this.discussion.line_code);
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__('MergeRequests|An error occurred while saving the draft comment.'),
});
});
@@ -82,7 +82,7 @@ export default {
}
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__('MergeRequests|An error occurred while saving the draft comment.'),
});
});
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index db5f9ebf3f0..d75a4158440 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,136 +1,12 @@
import { mapGetters, mapActions, mapState } from 'vuex';
-import { scrollToElementWithContext, scrollToElement, contentTop } from '~/lib/utils/common_utils';
-import { updateHistory } from '~/lib/utils/url_utility';
-import eventHub from '../event_hub';
-
-/**
- * @param {string} selector
- * @returns {boolean}
- */
-function scrollTo(selector, { withoutContext = false, offset = 0 } = {}) {
- const el = document.querySelector(selector);
- const scrollFunction = withoutContext ? scrollToElement : scrollToElementWithContext;
-
- if (el) {
- scrollFunction(el, {
- behavior: 'auto',
- offset,
- });
- return true;
- }
-
- return false;
-}
-
-function updateUrlWithNoteId(noteId) {
- const newHistoryEntry = {
- state: null,
- title: window.title,
- url: `#note_${noteId}`,
- replace: true,
- };
-
- if (noteId) {
- // Temporarily mask the ID to avoid the browser default
- // scrolling taking over which is broken with virtual
- // scrolling enabled.
- const note = document.querySelector(`#note_${noteId}`);
- note?.setAttribute('id', `masked::${note.id}`);
-
- // Update the hash now that the ID "doesn't exist" in the page
- updateHistory(newHistoryEntry);
-
- // Unmask the note's ID
- note?.setAttribute('id', `note_${noteId}`);
- }
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {string} id Discussion id we are jumping to
- */
-function diffsJump({ expandDiscussion }, id, firstNoteId) {
- const selector = `ul.notes[data-discussion-id="${id}"]`;
-
- eventHub.$once('scrollToDiscussion', () => {
- scrollTo(selector);
- // Wait for the discussion scroll before updating to the more specific ID
- setTimeout(() => updateUrlWithNoteId(firstNoteId), 0);
- });
- expandDiscussion({ discussionId: id });
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {string} id Discussion id we are jumping to
- * @returns {boolean}
- */
-function discussionJump({ expandDiscussion }, id) {
- const selector = `div.discussion[data-discussion-id="${id}"]`;
- expandDiscussion({ discussionId: id });
- return scrollTo(selector, {
- withoutContext: true,
- offset: window.gon?.features?.movedMrSidebar ? -28 : 0,
- });
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {string} id Discussion id we are jumping to
- */
-function switchToDiscussionsTabAndJumpTo(self, id) {
- window.mrTabs.eventHub.$once('MergeRequestTabChange', () => {
- setTimeout(() => discussionJump(self, id), 0);
- });
-
- window.mrTabs.tabShown('show');
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {object} discussion Discussion we are jumping to
- */
-function jumpToDiscussion(self, discussion) {
- const { id, diff_discussion: isDiffDiscussion, notes } = discussion;
- const firstNoteId = notes?.[0]?.id;
- if (id) {
- const activeTab = window.mrTabs.currentAction;
-
- if (activeTab === 'diffs' && isDiffDiscussion) {
- diffsJump(self, id, firstNoteId);
- } else {
- switchToDiscussionsTabAndJumpTo(self, id);
- }
- }
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {function} fn Which function used to get the target discussion's id
- */
-function handleDiscussionJump(self, fn) {
- const isDiffView = window.mrTabs.currentAction === 'diffs';
- const targetId = fn(self.currentDiscussionId, isDiffView);
- const discussion = self.getDiscussion(targetId);
- const discussionFilePath = discussion?.diff_file?.file_path;
-
- window.location.hash = '';
-
- if (discussionFilePath) {
- self.scrollToFile({
- path: discussionFilePath,
- });
- }
-
- self.$nextTick(() => {
- jumpToDiscussion(self, discussion);
- self.setCurrentDiscussionId(targetId);
- });
-}
+import { scrollToElement, contentTop } from '~/lib/utils/common_utils';
function getAllDiscussionElements() {
+ const containerEl = window.mrTabs?.currentAction === 'diffs' ? '.diffs' : '.notes';
return Array.from(
- document.querySelectorAll('[data-discussion-id]:not([data-discussion-resolved])'),
+ document.querySelectorAll(
+ `${containerEl} div[data-discussion-id]:not([data-discussion-resolved])`,
+ ),
);
}
@@ -182,14 +58,10 @@ function getPreviousDiscussion() {
}
function handleJumpForBothPages(getDiscussion, ctx, fn, scrollOptions) {
- if (window.mrTabs.currentAction !== 'show') {
- handleDiscussionJump(ctx, fn);
- } else {
- const discussion = getDiscussion();
- const id = discussion.dataset.discussionId;
- ctx.expandDiscussion({ discussionId: id });
- scrollToElement(discussion, scrollOptions);
- }
+ const discussion = getDiscussion();
+ const id = discussion.dataset.discussionId;
+ ctx.expandDiscussion({ discussionId: id });
+ scrollToElement(discussion, scrollOptions);
}
export default {
@@ -205,9 +77,11 @@ export default {
},
methods: {
...mapActions(['expandDiscussion', 'setCurrentDiscussionId']),
- ...mapActions('diffs', ['scrollToFile']),
+ ...mapActions('diffs', ['scrollToFile', 'disableVirtualScroller']),
+
+ async jumpToNextDiscussion(scrollOptions) {
+ await this.disableVirtualScroller();
- jumpToNextDiscussion(scrollOptions) {
handleJumpForBothPages(
getNextDiscussion,
this,
@@ -216,7 +90,9 @@ export default {
);
},
- jumpToPreviousDiscussion(scrollOptions) {
+ async jumpToPreviousDiscussion(scrollOptions) {
+ await this.disableVirtualScroller();
+
handleJumpForBothPages(
getPreviousDiscussion,
this,
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index 9783def1b46..44751020173 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
export default {
@@ -46,7 +46,7 @@ export default {
this.isResolving = false;
const msg = __('Something went wrong while resolving this discussion. Please try again.');
- createFlash({
+ createAlert({
message: msg,
parent: this.$el,
});
diff --git a/app/assets/javascripts/notes/timeline.js b/app/assets/javascripts/notes/timeline.js
deleted file mode 100644
index df6d1b21400..00000000000
--- a/app/assets/javascripts/notes/timeline.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Vue from 'vue';
-import TimelineToggle from './components/timeline_toggle.vue';
-
-export default function initTimelineToggle(store) {
- const el = document.getElementById('js-incidents-timeline-toggle');
-
- if (!el) return null;
-
- return new Vue({
- el,
- store,
- render(createElement) {
- return createElement(TimelineToggle);
- },
- });
-}
diff --git a/app/assets/javascripts/notes/utils/get_notes_filter_data.js b/app/assets/javascripts/notes/utils/get_notes_filter_data.js
new file mode 100644
index 00000000000..6d62ab5e91b
--- /dev/null
+++ b/app/assets/javascripts/notes/utils/get_notes_filter_data.js
@@ -0,0 +1,21 @@
+/**
+ * Returns parsed notes filter data from a given element's dataset
+ *
+ * @param {Element} el containing info in the dataset
+ */
+export const getNotesFilterData = (el) => {
+ const { notesFilterValue: valueData, notesFilters: filtersData } = el.dataset;
+
+ const filtersParsed = filtersData ? JSON.parse(filtersData) : {};
+ const filters = Object.keys(filtersParsed).map((key) => ({
+ title: key,
+ value: filtersParsed[key],
+ }));
+
+ const value = valueData ? Number(valueData) : undefined;
+
+ return {
+ notesFilters: filters,
+ notesFilterValue: value,
+ };
+};
diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js
index 529eb7d207b..5f60cab8bdd 100644
--- a/app/assets/javascripts/operation_settings/store/actions.js
+++ b/app/assets/javascripts/operation_settings/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -35,7 +35,7 @@ export const receiveSaveChangesError = (_, error) => {
const { response = {} } = error;
const message = response.data && response.data.message ? response.data.message : '';
- createFlash({
+ createAlert({
message: `${__('There was an error saving your changes.')} ${message}`,
});
};
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index 9e8eb92d87a..597df2b9bc3 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
@@ -1,6 +1,6 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { n__ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
@@ -69,7 +69,7 @@ export default {
return this.queryVariables;
},
error() {
- createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index 9ebbdfa920d..8b66165a57a 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
@@ -1,7 +1,7 @@
<script>
import { GlResizeObserverDirective, GlEmptyState } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
@@ -66,7 +66,7 @@ export default {
this.updateBreadcrumb();
},
error() {
- createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index c1bd71de646..794be8d5195 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
@@ -10,7 +10,7 @@ import {
} from '@gitlab/ui';
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import Tracking from '~/tracking';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
@@ -100,7 +100,7 @@ export default {
this.containerRepositoriesCount = data[this.graphqlResource]?.containerRepositoriesCount;
},
error() {
- createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
additionalDetails: {
@@ -115,7 +115,7 @@ export default {
return data[this.graphqlResource]?.containerRepositories.nodes;
},
error() {
- createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js
index 26d4aa13715..223f427ce0e 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import {
DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
@@ -20,7 +20,7 @@ export const fetchPackageVersions = ({ commit, state }) => {
}
})
.catch(() => {
- createFlash({ message: FETCH_PACKAGE_VERSIONS_ERROR, type: 'warning' });
+ createAlert({ message: FETCH_PACKAGE_VERSIONS_ERROR, variant: VARIANT_WARNING });
})
.finally(() => {
commit(types.SET_LOADING, false);
@@ -33,7 +33,7 @@ export const deletePackage = ({
},
}) => {
return Api.deleteProjectPackage(project_id, id).catch(() => {
- createFlash({ message: DELETE_PACKAGE_ERROR_MESSAGE, type: 'warning' });
+ createAlert({ message: DELETE_PACKAGE_ERROR_MESSAGE, variant: VARIANT_WARNING });
});
};
@@ -51,9 +51,9 @@ export const deletePackageFile = (
.then(() => {
const filtered = packageFiles.filter((f) => f.id !== fileId);
commit(types.UPDATE_PACKAGE_FILES, filtered);
- createFlash({ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, type: 'success' });
+ createAlert({ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, variant: VARIANT_SUCCESS });
})
.catch(() => {
- createFlash({ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, type: 'warning' });
+ createAlert({ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, variant: VARIANT_WARNING });
});
};
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
index dab4a051d0c..8b6a5c59847 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
@@ -81,10 +81,9 @@ export default {
},
},
i18n: {
- deleteModalContent: s__(
- 'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
- ),
- modalAction: s__('PackageRegistry|Delete package'),
+ deleteModalContent: s__('PackageRegistry|You are about to delete %{name}, are you sure?'),
+ modalTitle: s__('PackageRegistry|Delete package'),
+ modalAction: s__('PackageRegistry|Permanently delete'),
},
};
</script>
@@ -120,13 +119,13 @@ export default {
<gl-modal
ref="packageListDeleteModal"
size="sm"
- modal-id="confirm-delete-pacakge"
+ modal-id="confirm-delete-package"
:action-primary="deleteModalActionPrimaryProps"
:action-cancel="deleteModalActionCancelProps"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
>
- <template #modal-title>{{ $options.i18n.modalAction }}</template>
+ <template #modal-title>{{ $options.i18n.modalTitle }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #name>
<strong>{{ deletePackageName }}</strong>
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
index 184a24047eb..2adf6187c4b 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import {
@@ -84,7 +84,7 @@ export default {
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
if (showAlert) {
// to be refactored to use gl-alert
- createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' });
+ createAlert({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, variant: VARIANT_INFO });
const cleanUrl = window.location.href.split('?')[0];
historyReplaceState(cleanUrl);
}
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
index 51a38c434cb..37b51797490 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants';
import {
@@ -43,7 +43,7 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
dispatch('receivePackagesListSuccess', { data, headers });
})
.catch(() => {
- createFlash({
+ createAlert({
message: FETCH_PACKAGES_LIST_ERROR_MESSAGE,
});
})
@@ -54,7 +54,7 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
export const requestDeletePackage = ({ dispatch, state }, { _links }) => {
if (!_links || !_links.delete_api_path) {
- createFlash({
+ createAlert({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
const error = new Error(MISSING_DELETE_PATH_ERROR);
@@ -69,14 +69,14 @@ export const requestDeletePackage = ({ dispatch, state }, { _links }) => {
const page = getNewPaginationPage(currentPage, perPage, total - 1);
dispatch('requestPackagesList', { page });
- createFlash({
+ createAlert({
message: DELETE_PACKAGE_SUCCESS_MESSAGE,
- type: 'success',
+ variant: VARIANT_SUCCESS,
});
})
.catch(() => {
dispatch('setLoading', false);
- createFlash({
+ createAlert({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
});
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
index 11fd0db3106..cee976656f9 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
@@ -2,7 +2,8 @@
import { GlSprintf, GlBadge, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import { __ } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
+import { formatDate } from '~/lib/utils/datetime_utility';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants';
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
@@ -25,6 +26,7 @@ export default {
},
inject: ['isGroupPage'],
i18n: {
+ lastDownloadedAt: s__('PackageRegistry|Last downloaded %{dateTime}'),
packageInfo: __('v%{version} published %{timeAgo}'),
},
props: {
@@ -39,6 +41,11 @@ export default {
};
},
computed: {
+ packageLastDownloadedAtDisplay() {
+ return sprintf(this.$options.i18n.lastDownloadedAt, {
+ dateTime: formatDate(this.packageEntity.lastDownloadedAt, 'mmm d, yyyy'),
+ });
+ },
packageTypeDisplay() {
return getPackageTypeLabel(this.packageEntity.packageType);
},
@@ -136,6 +143,15 @@ export default {
<metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" />
</template>
+ <template v-if="packageEntity.lastDownloadedAt" #metadata-last-downloaded-at>
+ <metadata-item
+ data-testid="package-last-downloaded-at"
+ icon="download"
+ :text="packageLastDownloadedAtDisplay"
+ size="m"
+ />
+ </template>
+
<template #right-actions>
<slot name="delete-button"></slot>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue
index 7a85fd3052e..e1cf4883029 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue
@@ -1,6 +1,6 @@
<script>
import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import { s__ } from '~/locale';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/package_registry/constants';
@@ -39,15 +39,15 @@ export default {
throw data.destroyPackage.errors[0];
}
if (this.showSuccessAlert) {
- createFlash({
+ createAlert({
message: this.$options.i18n.successMessage,
- type: 'success',
+ variant: VARIANT_SUCCESS,
});
}
} catch (error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.errorMessage,
- type: 'warning',
+ variant: VARIANT_WARNING,
captureError: true,
error,
});
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
index e84f181e9b2..c6583b8f09f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -122,10 +122,9 @@ export default {
},
},
i18n: {
- deleteModalContent: s__(
- 'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
- ),
- modalAction: s__('PackageRegistry|Delete package'),
+ deleteModalContent: s__('PackageRegistry|You are about to delete %{name}, are you sure?'),
+ modalTitle: s__('PackageRegistry|Delete package'),
+ modalAction: s__('PackageRegistry|Permanently delete'),
errorMessageBodyAlert: s__(
'PackageRegistry|There was a timeout and the package was not published. Delete this package and try again.',
),
@@ -172,14 +171,14 @@ export default {
<gl-modal
v-model="showDeleteModal"
- modal-id="confirm-delete-pacakge"
+ modal-id="confirm-delete-package"
size="sm"
:action-primary="deleteModalActionPrimaryProps"
:action-cancel="deleteModalActionCancelProps"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
>
- <template #modal-title>{{ $options.i18n.modalAction }}</template>
+ <template #modal-title>{{ $options.i18n.modalTitle }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #name>
<strong>{{ deletePackageName }}</strong>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index 06a04ee248a..4e35176c757 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -78,6 +78,17 @@ export const TRACKING_ACTION_CLICK_COMMIT_LINK = 'click_commit_link_from_package
export const TRACKING_LABEL_PACKAGE_HISTORY = 'package_history';
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
+
+export const DELETE_MODAL_TITLE = s__('PackageRegistry|Delete package version');
+export const DELETE_MODAL_CONTENT = s__(
+ `PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`,
+);
+export const DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT = s__(
+ `PackageRegistry|Deleting all package assets will remove version %{version} of %{name}. Are you sure?`,
+);
+export const DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT = s__(
+ `PackageRegistry|Deleting the last package asset will remove version %{version} of %{name}. Are you sure?`,
+);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package asset.',
);
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index f3f0d096d10..8e50c95b10b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -4,6 +4,7 @@ query getPackageDetails($id: PackagesPackageID!) {
name
packageType
version
+ lastDownloadedAt
createdAt
updatedAt
status
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index c10fc914d56..eeed56b77c3 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -10,7 +10,7 @@ import {
GlTabs,
GlSprintf,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
@@ -44,6 +44,10 @@ import {
DELETE_PACKAGE_FILES_ERROR_MESSAGE,
DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
+ DELETE_MODAL_TITLE,
+ DELETE_MODAL_CONTENT,
+ DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
+ DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT,
} from '~/packages_and_registries/package_registry/constants';
import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
@@ -86,6 +90,7 @@ export default {
},
data() {
return {
+ deletePackageModalContent: DELETE_MODAL_CONTENT,
filesToDelete: [],
mutationLoading: false,
packageEntity: {},
@@ -101,7 +106,7 @@ export default {
return data.package || {};
},
error(error) {
- createFlash({
+ createAlert({
message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
captureError: true,
error,
@@ -205,20 +210,18 @@ export default {
if (data?.destroyPackageFiles?.errors[0]) {
throw data.destroyPackageFiles.errors[0];
}
- createFlash({
- message:
- ids.length === 1
- ? DELETE_PACKAGE_FILE_SUCCESS_MESSAGE
- : DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
- type: 'success',
+ createAlert({
+ message: this.isLastItem(ids)
+ ? DELETE_PACKAGE_FILE_SUCCESS_MESSAGE
+ : DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ variant: VARIANT_SUCCESS,
});
} catch (error) {
- createFlash({
- message:
- ids.length === 1
- ? DELETE_PACKAGE_FILE_ERROR_MESSAGE
- : DELETE_PACKAGE_FILES_ERROR_MESSAGE,
- type: 'warning',
+ createAlert({
+ message: this.isLastItem(ids)
+ ? DELETE_PACKAGE_FILE_ERROR_MESSAGE
+ : DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ variant: VARIANT_WARNING,
captureError: true,
error,
});
@@ -231,18 +234,26 @@ export default {
files.length === this.packageFiles.length &&
!this.packageEntity.packageFiles?.pageInfo?.hasNextPage
) {
+ if (this.isLastItem(files)) {
+ this.deletePackageModalContent = DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT;
+ } else {
+ this.deletePackageModalContent = DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT;
+ }
this.$refs.deleteModal.show();
} else {
this.filesToDelete = files;
- if (files.length === 1) {
+ if (this.isLastItem(files)) {
this.$refs.deleteFileModal.show();
} else if (files.length > 1) {
this.$refs.deleteFilesModal.show();
}
}
},
+ isLastItem(items) {
+ return items.length === 1;
+ },
confirmFilesDelete() {
- if (this.filesToDelete.length === 1) {
+ if (this.isLastItem(this.filesToDelete)) {
this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION);
} else {
this.track(DELETE_PACKAGE_FILES_TRACKING_ACTION);
@@ -250,12 +261,12 @@ export default {
this.deletePackageFiles(this.filesToDelete.map((file) => file.id));
this.filesToDelete = [];
},
+ resetDeleteModalContent() {
+ this.deletePackageModalContent = DELETE_MODAL_CONTENT;
+ },
},
i18n: {
- deleteModalTitle: s__(`PackageRegistry|Delete Package Version`),
- deleteModalContent: s__(
- `PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`,
- ),
+ DELETE_MODAL_TITLE,
deleteFileModalTitle: s__(`PackageRegistry|Delete package asset`),
deleteFileModalContent: s__(
`PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
@@ -263,7 +274,7 @@ export default {
},
modal: {
packageDeletePrimaryAction: {
- text: __('Delete'),
+ text: s__('PackageRegistry|Permanently delete'),
attributes: [
{ variant: 'danger' },
{ category: 'primary' },
@@ -371,10 +382,11 @@ export default {
:action-primary="$options.modal.packageDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction"
@primary="deletePackage(packageEntity)"
+ @hidden="resetDeleteModalContent"
@canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE)"
>
- <template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
- <gl-sprintf :message="$options.i18n.deleteModalContent">
+ <template #modal-title>{{ $options.i18n.DELETE_MODAL_TITLE }}</template>
+ <gl-sprintf :message="deletePackageModalContent">
<template #version>
<strong>{{ packageEntity.version }}</strong>
</template>
@@ -398,7 +410,7 @@ export default {
@canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)"
>
<template #modal-title>{{ $options.i18n.deleteFileModalTitle }}</template>
- <gl-sprintf v-if="filesToDelete.length === 1" :message="$options.i18n.deleteFileModalContent">
+ <gl-sprintf v-if="isLastItem(filesToDelete)" :message="$options.i18n.deleteFileModalContent">
<template #filename>
<strong>{{ filesToDelete[0].fileName }}</strong>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
index 38df701157a..ed9ab0367dd 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
@@ -1,6 +1,6 @@
<script>
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
@@ -105,7 +105,7 @@ export default {
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
if (showAlert) {
// to be refactored to use gl-alert
- createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' });
+ createAlert({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, variant: VARIANT_INFO });
const cleanUrl = window.location.href.split('?')[0];
historyReplaceState(cleanUrl);
}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue
index 72e68aca070..b8405b09840 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue
@@ -57,6 +57,9 @@ export default {
isEnabled() {
return this.containerExpirationPolicy || this.enableHistoricEntries;
},
+ isLoading() {
+ return this.$apollo.queries.containerExpirationPolicy.loading;
+ },
showDisabledFormMessage() {
return !this.isEnabled && !this.fetchSettingsError;
},
@@ -86,10 +89,10 @@ export default {
<container-expiration-policy-form
v-if="isEnabled"
v-model="workingCopy"
- :is-loading="$apollo.queries.containerExpirationPolicy.loading"
+ :is-loading="isLoading"
:is-edited="isEdited"
/>
- <template v-else>
+ <template v-if="!isLoading">
<gl-alert
v-if="showDisabledFormMessage"
:dismissible="false"
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
index b003b6aeb6b..1dd88d69d30 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
@@ -110,7 +110,7 @@ export default {
{{ cleanupRulesButtonText }}
</gl-button>
</gl-card>
- <template v-else>
+ <template v-if="!$apollo.queries.containerExpirationPolicy.loading">
<gl-alert
v-if="showDisabledFormMessage"
:dismissible="false"
diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js b/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js
index 8cecc1d3ef7..97fb64f9971 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -29,7 +29,7 @@ export default class PayloadDownloader {
PayloadDownloader.downloadFile(data);
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Error fetching payload data.'),
});
})
diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
index 616005565c4..1cd19fc09a8 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -43,7 +43,7 @@ export default class PayloadPreviewer {
})
.catch(() => {
this.spinner.classList.remove('gl-display-inline');
- createFlash({
+ createAlert({
message: __('Error fetching payload data.'),
});
});
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
index 18ba89f8856..40348e0b18a 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
@@ -30,7 +30,7 @@ export default () => {
$jsBroadcastMessagePreview.html(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while rendering preview broadcast message'),
}),
);
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
index f687423594d..ffd976be8c6 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
@@ -1,5 +1,10 @@
+import initBroadcastMessages from '~/admin/broadcast_messages';
import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
import initBroadcastMessagesForm from './broadcast_message';
-initBroadcastMessagesForm();
-initDeprecatedRemoveRowBehavior();
+if (gon.features.vueBroadcastMessages) {
+ initBroadcastMessages();
+} else {
+ initBroadcastMessagesForm();
+ initDeprecatedRemoveRowBehavior();
+}
diff --git a/app/assets/javascripts/pages/admin/dashboard/index.js b/app/assets/javascripts/pages/admin/dashboard/index.js
new file mode 100644
index 00000000000..b63e612be47
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/dashboard/index.js
@@ -0,0 +1,3 @@
+import initGitlabVersionCheck from '~/gitlab_version_check';
+
+initGitlabVersionCheck();
diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js
deleted file mode 100644
index 86b80a0ba5b..00000000000
--- a/app/assets/javascripts/pages/admin/groups/show/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import UsersSelect from '~/users_select';
-
-new UsersSelect(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js
index a249864fa36..eaee625c047 100644
--- a/app/assets/javascripts/pages/admin/index.js
+++ b/app/assets/javascripts/pages/admin/index.js
@@ -1,10 +1,8 @@
-import initGitlabVersionCheck from '~/gitlab_version_check';
import initAdminStatisticsPanel from '~/admin/statistics_panel/index';
import initVueAlerts from '~/vue_alerts';
import initAdmin from './admin';
initVueAlerts();
-initGitlabVersionCheck();
const statisticsPanelContainer = document.getElementById('js-admin-statistics-container');
initAdmin();
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index 63b98f4143b..4f42ef2892d 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -31,7 +31,7 @@ export default {
redirectTo(response.request.responseURL);
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__('AdminArea|Stopping jobs failed'),
});
throw error;
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index b6f42a27002..2a7619da8cc 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -4,7 +4,7 @@ import $ from 'jquery';
import { getGroups } from '~/api/groups_api';
import { getProjects } from '~/api/projects_api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { isMetaClick } from '~/lib/utils/common_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
@@ -119,7 +119,7 @@ export default class Todos {
})
.catch(() => {
this.updateRowState(target, true);
- return createFlash({
+ return createAlert({
message: __('Error updating status of to-do item.'),
});
});
@@ -168,7 +168,7 @@ export default class Todos {
this.updateBadges(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error updating status for all to-do items.'),
}),
);
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index de28f027126..377ba0f13a9 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -1,6 +1,10 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import { initBulkUpdateSidebar } from '~/issuable/bulk_update_sidebar';
+import {
+ initBulkUpdateSidebar,
+ initStatusDropdown,
+ initSubscriptionsDropdown,
+} from '~/issuable/bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
@@ -9,6 +13,8 @@ const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
initBulkUpdateSidebar(ISSUABLE_BULK_UPDATE_PREFIX);
+initStatusDropdown();
+initSubscriptionsDropdown();
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
index 8ce73be6e74..fa111032b2e 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -1,6 +1,6 @@
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import InputValidator from '~/validators/input_validator';
import { getGroupPathAvailability } from '~/rest_api';
@@ -62,7 +62,7 @@ export default class GroupPathValidator extends InputValidator {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while validating group path'),
}),
);
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 7dab5258b24..a555038ed5c 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import BindInOut from '~/behaviors/bind_in_out';
import initFilePickers from '~/file_pickers';
import Group from '~/group';
@@ -8,6 +10,8 @@ import NewGroupCreationApp from './components/app.vue';
import GroupPathValidator from './group_path_validator';
import initToggleInviteMembers from './toggle_invite_members';
+Vue.use(VueApollo);
+
new GroupPathValidator(); // eslint-disable-line no-new
new Group(); // eslint-disable-line no-new
initGroupNameAndPath();
@@ -31,8 +35,13 @@ function initNewGroupCreation(el) {
hasErrors: parseBoolean(hasErrors),
};
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
return new Vue({
el,
+ apolloProvider,
provide: {
verificationRequired: parseBoolean(verificationRequired),
verificationFormUrl,
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index bf77d968e7d..b1a1cc21764 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -2,9 +2,11 @@ import initStaleRunnerCleanupSetting from 'ee_else_ce/group_settings/stale_runne
import initVariableList from '~/ci_variable_list';
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
import initSettingsPanels from '~/settings_panels';
+import initDeployTokens from '~/deploy_tokens';
// Initialize expandable settings panels
initSettingsPanels();
+initDeployTokens();
initSharedRunnersForm();
initStaleRunnerCleanupSetting();
diff --git a/app/assets/javascripts/pages/groups/settings/index.js b/app/assets/javascripts/pages/groups/settings/index.js
index cb787c60002..7e97cd865b7 100644
--- a/app/assets/javascripts/pages/groups/settings/index.js
+++ b/app/assets/javascripts/pages/groups/settings/index.js
@@ -1,5 +1,7 @@
import initRevokeButton from '~/deploy_tokens/init_revoke_button';
import initSearchSettings from '~/search_settings';
+import initDeployTokens from '~/deploy_tokens';
+initDeployTokens();
initSearchSettings();
initRevokeButton();
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 9cce6723bf7..6feb4c2188f 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
@@ -2,7 +2,7 @@
import { GlButton, GlEmptyState, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { s__, __ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { getBulkImportsHistory } from '~/rest_api';
@@ -107,7 +107,7 @@ export default {
this.pageInfo = parseIntPagination(normalizeHeaders(headers));
this.historyItems = historyItems;
} catch (e) {
- createFlash({ message: DEFAULT_ERROR, captureError: true, error: e });
+ createAlert({ message: DEFAULT_ERROR, captureError: true, error: e });
} finally {
this.loading = false;
}
diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
new file mode 100644
index 00000000000..20ce296bbec
--- /dev/null
+++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlAvatarLabeled, GlListbox } from '@gitlab/ui';
+import { __ } from '~/locale';
+import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+const USERS_PER_PAGE = 20;
+
+export default {
+ components: {
+ GlAvatarLabeled,
+ GlListbox,
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ usersQuery: {
+ query: searchUsersQuery,
+ variables() {
+ return {
+ search: this.search,
+ first: USERS_PER_PAGE,
+ };
+ },
+ update(data) {
+ return data;
+ },
+ debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+ },
+ },
+ data() {
+ return {
+ user: '',
+ search: '',
+ };
+ },
+ computed: {
+ userId() {
+ return getIdFromGraphQLId(this.user);
+ },
+ users() {
+ return [
+ { text: __('(no user)'), value: '' },
+ ...(this.usersQuery?.users.nodes || []).map((u) => ({
+ username: `@${u.username}`,
+ avatarUrl: u.avatarUrl,
+ text: u.name,
+ value: u.id,
+ })),
+ ];
+ },
+ },
+ methods: {
+ clearTransform() {
+ // FIXME: workaround for listbox issue
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1986
+ const { listbox } = this.$refs;
+ if (listbox.querySelector('.dropdown-menu')) {
+ listbox.querySelector('.dropdown-menu').style.transform = '';
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-listbox
+ ref="listbox"
+ v-model="user"
+ :items="users"
+ searchable
+ is-check-centered
+ :searching="$apollo.loading"
+ @click.capture.native="clearTransform"
+ @search="search = $event"
+ >
+ <template #list-item="{ item }">
+ <gl-avatar-labeled
+ shape="circle"
+ :size="32"
+ :src="item.avatarUrl"
+ :label="item.text"
+ :sub-label="item.username"
+ />
+ </template>
+ </gl-listbox>
+ <input type="hidden" :name="name" :value="userId" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
index 86b80a0ba5b..ef549f20cf3 100644
--- a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
+++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
@@ -1,3 +1,19 @@
-import UsersSelect from '~/users_select';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import UserSelect from './components/user_select.vue';
-new UsersSelect(); // eslint-disable-line no-new
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+Array.from(document.querySelectorAll('.js-gitlab-user')).forEach(
+ (node) =>
+ new Vue({
+ el: node,
+ apolloProvider,
+ render: (h) => h(UserSelect, { props: { name: node.dataset.name } }),
+ }),
+);
diff --git a/app/assets/javascripts/pages/import/history/components/import_history_app.vue b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
index db6f0c23dbd..09b1b3a9c0f 100644
--- a/app/assets/javascripts/pages/import/history/components/import_history_app.vue
+++ b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlEmptyState, GlIcon, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { s__, __ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { getProjects } from '~/rest_api';
import ImportStatus from '~/import_entities/components/import_status.vue';
@@ -104,7 +104,7 @@ export default {
this.pageInfo = parseIntPagination(normalizeHeaders(headers));
this.historyItems = historyItems;
} catch (e) {
- createFlash({ message: DEFAULT_ERROR, captureError: true, error: e });
+ createAlert({ message: DEFAULT_ERROR, captureError: true, error: e });
} finally {
this.loading = false;
}
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
index 6afb3636998..91b20a05196 100644
--- a/app/assets/javascripts/pages/profiles/index.js
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -3,6 +3,7 @@ import '~/profile/gl_crop';
import Profile from '~/profile/profile';
import initSearchSettings from '~/search_settings';
import initPasswordPrompt from './password_prompt';
+import { initTimezoneDropdown } from './init_timezone_dropdown';
// eslint-disable-next-line func-names
$(document).on('input.ssh_key', '#key_key', function () {
@@ -21,3 +22,4 @@ new Profile(); // eslint-disable-line no-new
initSearchSettings();
initPasswordPrompt();
+initTimezoneDropdown();
diff --git a/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js b/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js
new file mode 100644
index 00000000000..80b911493a8
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
+
+export const initTimezoneDropdown = () => {
+ const el = document.querySelector('.js-timezone-dropdown');
+
+ if (!el) {
+ return null;
+ }
+
+ const { timezoneData, initialValue } = el.dataset;
+ const timezones = JSON.parse(timezoneData);
+
+ const timezoneDropdown = new Vue({
+ el,
+ data() {
+ return {
+ value: initialValue,
+ };
+ },
+ render(h) {
+ return h(TimezoneDropdown, {
+ props: {
+ value: this.value,
+ timezoneData: timezones,
+ name: 'user[timezone]',
+ },
+ class: 'gl-md-form-input-lg',
+ });
+ },
+ });
+
+ return timezoneDropdown;
+};
diff --git a/app/assets/javascripts/pages/projects/blame/show/index.js b/app/assets/javascripts/pages/projects/blame/show/index.js
index fa22c11d1d7..1e4b9de90f2 100644
--- a/app/assets/javascripts/pages/projects/blame/show/index.js
+++ b/app/assets/javascripts/pages/projects/blame/show/index.js
@@ -1,3 +1,5 @@
import initBlob from '~/pages/projects/init_blob';
+import redirectToCorrectPage from '~/blame/blame_redirect';
+redirectToCorrectPage();
initBlob();
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index eca3cf7ab13..af0097b415c 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import initDeprecatedNotes from '~/init_deprecated_notes';
import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import axios from '~/lib/utils/axios_utils';
@@ -69,7 +69,7 @@ if (filesContainer.length) {
loadDiffStats();
})
.catch(() => {
- createFlash({ message: __('An error occurred while retrieving diff files') });
+ createAlert({ message: __('An error occurred while retrieving diff files') });
});
} else {
new Diff();
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index b415e36bf09..30cefa3d717 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -12,7 +12,7 @@ import {
} from '@gitlab/ui';
import { kebabCase } from 'lodash';
import { buildApiUrl } from '~/api/api_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -220,7 +220,7 @@ export default {
redirectTo(data.web_url);
return;
} catch (error) {
- createFlash({
+ createAlert({
message: s__(
'ForkProject|An error occurred while forking the project. Please try again.',
),
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue b/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue
index 2b3055ecd66..00e0649deed 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue
@@ -9,7 +9,7 @@ import {
GlSearchBoxByType,
GlTruncate,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -41,7 +41,7 @@ export default {
return length > 0 && length < MINIMUM_SEARCH_LENGTH;
},
error(error) {
- createFlash({
+ createAlert({
message: s__(
'ForkProject|Something went wrong while loading data. Please refresh the page to try again.',
),
diff --git a/app/assets/javascripts/pages/projects/hooks/index.js b/app/assets/javascripts/pages/projects/hooks/index.js
index 2a120a690ef..ed476d25f8b 100644
--- a/app/assets/javascripts/pages/projects/hooks/index.js
+++ b/app/assets/javascripts/pages/projects/hooks/index.js
@@ -1,3 +1,5 @@
import initSearchSettings from '~/search_settings';
+import initWebhookForm from '~/webhooks';
initSearchSettings();
+initWebhookForm();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
index 9a38c2cc765..65942464e2b 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
@@ -39,7 +39,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error fetching refs'),
}),
);
diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
index 5179d1b31ab..406959c80ea 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -33,7 +33,7 @@ function initTargetBranchSelector() {
callback(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error fetching branches'),
}),
);
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index e284e7b2c5e..2399aafc9b5 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -2,14 +2,19 @@ import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
-import { initBulkUpdateSidebar, initIssueStatusSelect } from '~/issuable/bulk_update_sidebar';
+import {
+ initBulkUpdateSidebar,
+ initStatusDropdown,
+ initSubscriptionsDropdown,
+} from '~/issuable/bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import { ISSUABLE_INDEX } from '~/issuable/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import UsersSelect from '~/users_select';
initBulkUpdateSidebar(ISSUABLE_INDEX.MERGE_REQUEST);
-initIssueStatusSelect();
+initStatusDropdown();
+initSubscriptionsDropdown();
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
index 6dd21380bec..0edce2db0a3 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
@@ -1,3 +1,8 @@
+import initPipelineSchedulesFormApp from '~/pipeline_schedules/mount_pipeline_schedules_form_app';
import initForm from '../shared/init_form';
-initForm();
+if (gon.features?.pipelineSchedulesVue) {
+ initPipelineSchedulesFormApp('#pipeline-schedules-form-edit');
+} else {
+ initForm();
+}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
index 9513f42d9c9..7d0930f6424 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
@@ -1,9 +1,10 @@
import Vue from 'vue';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
+import initPipelineSchedulesApp from '~/pipeline_schedules/mount_pipeline_schedules_app';
import PipelineSchedulesTakeOwnershipModal from '~/pipeline_schedules/components/take_ownership_modal.vue';
import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
-function initPipelineSchedules() {
+function initPipelineSchedulesCallout() {
const el = document.getElementById('pipeline-schedules-callout');
if (!el) {
@@ -15,6 +16,7 @@ function initPipelineSchedules() {
// eslint-disable-next-line no-new
new Vue({
el,
+ name: 'PipelineSchedulesCalloutRoot',
provide: {
docsUrl,
illustrationUrl,
@@ -25,6 +27,8 @@ function initPipelineSchedules() {
});
}
+// TODO: move take ownership feature into new Vue app
+// located in directory app/assets/javascripts/pipeline_schedules/components
function initTakeownershipModal() {
const modalId = 'pipeline-take-ownership-modal';
const buttonSelector = 'js-take-ownership-button';
@@ -63,5 +67,10 @@ function initTakeownershipModal() {
});
}
-initPipelineSchedules();
-initTakeownershipModal();
+initPipelineSchedulesCallout();
+
+if (gon.features?.pipelineSchedulesVue) {
+ initPipelineSchedulesApp();
+} else {
+ initTakeownershipModal();
+}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
index 6dd21380bec..06084fa729b 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
@@ -1,3 +1,8 @@
+import initPipelineSchedulesFormApp from '~/pipeline_schedules/mount_pipeline_schedules_form_app';
import initForm from '../shared/init_form';
-initForm();
+if (gon.features?.pipelineSchedulesVue) {
+ initPipelineSchedulesFormApp('#pipeline-schedules-form-new');
+} else {
+ initForm();
+}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
index 277d2e0d30a..bc467952551 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
@@ -1,4 +1,5 @@
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { formatTimezone } from '~/lib/utils/datetime_utility';
const defaultTimezone = { identifier: 'Etc/UTC', name: 'UTC', offset: 0 };
const defaults = {
@@ -17,8 +18,6 @@ export const formatUtcOffset = (offset) => {
return `${prefix} ${Math.abs(offset / 3600)}`;
};
-export const formatTimezone = (item) => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`;
-
export const findTimezoneByIdentifier = (tzList = [], identifier = null) => {
if (tzList && tzList.length && identifier && identifier.length) {
return tzList.find((tz) => tz.identifier === identifier) || null;
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index ccabaad5b2e..d177c67f133 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -4,7 +4,7 @@ import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
import initClonePanel from '~/clone_panel';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { serializeForm } from '~/lib/utils/forms';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -80,7 +80,7 @@ export default class Project {
})
.then(({ data }) => callback(data))
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while getting projects'),
}),
);
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 6a9bd34db22..8909ff1f221 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
@@ -10,6 +10,7 @@ import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_to
import initSettingsPanels from '~/settings_panels';
import { initTokenAccess } from '~/token_access';
import { initCiSecureFiles } from '~/ci_secure_files';
+import initDeployTokens from '~/deploy_tokens';
// Initialize expandable settings panels
initSettingsPanels();
@@ -34,6 +35,7 @@ document.querySelector('.js-toggle-extra-settings').addEventListener('click', (e
});
registrySettingsApp();
+initDeployTokens();
initDeployFreeze();
initSettingsPipelinesTriggers();
diff --git a/app/assets/javascripts/pages/projects/settings/index.js b/app/assets/javascripts/pages/projects/settings/index.js
index cb787c60002..7e97cd865b7 100644
--- a/app/assets/javascripts/pages/projects/settings/index.js
+++ b/app/assets/javascripts/pages/projects/settings/index.js
@@ -1,5 +1,7 @@
import initRevokeButton from '~/deploy_tokens/init_revoke_button';
import initSearchSettings from '~/search_settings';
+import initDeployTokens from '~/deploy_tokens';
+initDeployTokens();
initSearchSettings();
initRevokeButton();
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index 655243eee30..d2263fa815d 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,5 +1,7 @@
import MirrorRepos from '~/mirrors/mirror_repos';
import mountBranchRules from '~/projects/settings/repository/branch_rules/mount_branch_rules';
+import mountDefaultBranchSelector from '~/projects/settings/mount_default_branch_selector';
+
import initForm from '../form';
initForm();
@@ -8,3 +10,4 @@ const mirrorReposContainer = document.querySelector('.js-mirror-settings');
if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();
mountBranchRules(document.getElementById('js-branch-rules'));
+mountDefaultBranchSelector(document.querySelector('.js-select-default-branch'));
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index a82f485bf44..3e5c02bbf19 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -898,7 +898,9 @@ export default {
:help-path="pagesHelpPath"
:label="$options.i18n.pagesLabel"
:help-text="
- s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab.')
+ s__(
+ 'ProjectSettings|With GitLab Pages you can host your static websites on GitLab. GitLab Pages uses a caching mechanism for efficiency. Your changes may not take effect until that cache is invalidated, which usually takes less than a minute.',
+ )
"
>
<project-feature-setting
@@ -979,20 +981,20 @@ export default {
name="project[project_feature_attributes][feature_flags_access_level]"
/>
</project-setting-row>
- <project-setting-row
- ref="releases-settings"
- :label="$options.i18n.releasesLabel"
- :help-text="$options.i18n.releasesHelpText"
- :help-path="releasesHelpPath"
- >
- <project-feature-setting
- v-model="releasesAccessLevel"
- :label="$options.i18n.releasesLabel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][releases_access_level]"
- />
- </project-setting-row>
</template>
+ <project-setting-row
+ ref="releases-settings"
+ :label="$options.i18n.releasesLabel"
+ :help-text="$options.i18n.releasesHelpText"
+ :help-path="releasesHelpPath"
+ >
+ <project-feature-setting
+ v-model="releasesAccessLevel"
+ :label="$options.i18n.releasesLabel"
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][releases_access_level]"
+ />
+ </project-setting-row>
</div>
<project-setting-row v-if="canDisableEmails" ref="email-settings" class="mb-3">
<label class="js-emails-disabled">
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index 7ea744a68a6..1848aa70cf0 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -1,6 +1,6 @@
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import InputValidator from '~/validators/input_validator';
@@ -51,7 +51,7 @@ export default class UsernameValidator extends InputValidator {
);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while validating username'),
}),
);
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
index 10b95fd6f3c..b72579276e8 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
@@ -1,6 +1,6 @@
<script>
import { GlSkeletonLoader, GlSafeHtmlDirective, GlAlert } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { handleLocationHash } from '~/lib/utils/common_utils';
@@ -47,7 +47,7 @@ export default {
handleLocationHash();
})
.catch(() =>
- createFlash({
+ createAlert({
message: this.$options.i18n.renderingContentFailed,
}),
);
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 9acc1cb62a1..7b9656de362 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -8,21 +8,19 @@ import {
GlFormGroup,
GlFormInput,
GlFormSelect,
- GlSegmentedControl,
} from '@gitlab/ui';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import axios from '~/lib/utils/axios_utils';
+import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import csrf from '~/lib/utils/csrf';
import { setUrlFragment } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import {
- CONTENT_EDITOR_LOADED_ACTION,
SAVED_USING_CONTENT_EDITOR_ACTION,
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
WIKI_FORMAT_LABEL,
WIKI_FORMAT_UPDATED_ACTION,
+ CONTENT_EDITOR_LOADED_ACTION,
} from '../constants';
const trackingMixin = Tracking.mixin({
@@ -36,6 +34,29 @@ const MARKDOWN_LINK_TEXT = {
org: '[[page-slug]]',
};
+function getPagePath(pageInfo) {
+ return pageInfo.persisted ? pageInfo.path : pageInfo.createPath;
+}
+
+const autosaveKey = (pageInfo, field) => {
+ const path = pageInfo.persisted ? pageInfo.path : pageInfo.createPath;
+
+ return `${path}/${field}`;
+};
+
+const titleAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'title');
+const formatAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'format');
+const contentAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'content');
+const commitAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'commit');
+
+const getTitle = (pageInfo) => getDraft(titleAutosaveKey(pageInfo)) || pageInfo.title?.trim() || '';
+const getFormat = (pageInfo) =>
+ getDraft(formatAutosaveKey(pageInfo)) || pageInfo.format || 'markdown';
+const getContent = (pageInfo) => getDraft(contentAutosaveKey(pageInfo)) || pageInfo.content || '';
+const getCommitMessage = (pageInfo) =>
+ getDraft(commitAutosaveKey(pageInfo)) || pageInfo.commitMessage || '';
+const getIsFormDirty = (pageInfo) => Boolean(getDraft(titleAutosaveKey(pageInfo)));
+
export default {
i18n: {
title: {
@@ -74,10 +95,6 @@ export default {
},
cancel: s__('WikiPage|Cancel'),
},
- switchEditingControlOptions: [
- { text: s__('Wiki Page|Source'), value: 'source' },
- { text: s__('Wiki Page|Rich text'), value: 'richText' },
- ],
components: {
GlIcon,
GlForm,
@@ -87,26 +104,21 @@ export default {
GlSprintf,
GlLink,
GlButton,
- GlSegmentedControl,
- MarkdownField,
- LocalStorageSync,
- ContentEditor: () =>
- import(
- /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
- ),
+ MarkdownEditor,
},
mixins: [trackingMixin],
inject: ['formatOptions', 'pageInfo'],
data() {
return {
editingMode: 'source',
- title: this.pageInfo.title?.trim() || '',
- format: this.pageInfo.format || 'markdown',
- content: this.pageInfo.content || '',
- commitMessage: '',
- isDirty: false,
+ title: getTitle(this.pageInfo),
+ format: getFormat(this.pageInfo),
+ content: getContent(this.pageInfo),
+ commitMessage: getCommitMessage(this.pageInfo),
contentEditorEmpty: false,
+ isContentEditorActive: false,
switchEditingControlDisabled: false,
+ isFormDirty: getIsFormDirty(this.pageInfo),
};
},
computed: {
@@ -117,7 +129,7 @@ export default {
return csrf.token;
},
formAction() {
- return this.pageInfo.persisted ? this.pageInfo.path : this.pageInfo.createPath;
+ return getPagePath(this.pageInfo);
},
helpPath() {
return setUrlFragment(
@@ -162,15 +174,9 @@ export default {
disableSubmitButton() {
return this.noContent || !this.title;
},
- isContentEditorActive() {
- return this.isMarkdownFormat && this.useContentEditor;
- },
- useContentEditor() {
- return this.editingMode === 'richText';
- },
},
mounted() {
- this.updateCommitMessage();
+ if (!this.commitMessage) this.updateCommitMessage();
window.addEventListener('beforeunload', this.onPageUnload);
},
@@ -178,51 +184,45 @@ export default {
window.removeEventListener('beforeunload', this.onPageUnload);
},
methods: {
- renderMarkdown(content) {
- return axios
- .post(this.pageInfo.markdownPreviewPath, { text: content })
- .then(({ data }) => data.body);
- },
-
- setEditingMode(editingMode) {
- this.editingMode = editingMode;
- },
-
async handleFormSubmit(e) {
- e.preventDefault();
+ this.isFormDirty = false;
- if (this.useContentEditor) {
- this.trackFormSubmit();
- }
+ e.preventDefault();
+ this.trackFormSubmit();
this.trackWikiFormat();
// Wait until form field values are refreshed
await this.$nextTick();
e.target.submit();
+ },
- this.isDirty = false;
+ updateDrafts() {
+ updateDraft(titleAutosaveKey(this.pageInfo), this.title);
+ updateDraft(formatAutosaveKey(this.pageInfo), this.format);
+ updateDraft(contentAutosaveKey(this.pageInfo), this.content);
+ updateDraft(commitAutosaveKey(this.pageInfo), this.commitMessage);
},
- handleContentChange() {
- this.isDirty = true;
+ clearDrafts() {
+ clearDraft(titleAutosaveKey(this.pageInfo));
+ clearDraft(formatAutosaveKey(this.pageInfo));
+ clearDraft(contentAutosaveKey(this.pageInfo));
+ clearDraft(commitAutosaveKey(this.pageInfo));
},
- handleContentEditorChange({ empty, markdown, changed }) {
+ handleContentEditorChange({ empty, markdown }) {
this.contentEditorEmpty = empty;
- this.isDirty = changed;
this.content = markdown;
},
- onPageUnload(event) {
- if (!this.isDirty) return undefined;
-
- event.preventDefault();
-
- // eslint-disable-next-line no-param-reassign
- event.returnValue = '';
- return '';
+ onPageUnload() {
+ if (this.isFormDirty) {
+ this.updateDrafts();
+ } else {
+ this.clearDrafts();
+ }
},
updateCommitMessage() {
@@ -235,8 +235,13 @@ export default {
this.commitMessage = newCommitMessage;
},
- trackContentEditorLoaded() {
- this.track(CONTENT_EDITOR_LOADED_ACTION);
+ notifyContentEditorActive() {
+ this.isContentEditorActive = true;
+ this.trackContentEditorLoaded();
+ },
+
+ notifyContentEditorInactive() {
+ this.isContentEditorActive = false;
},
trackFormSubmit() {
@@ -256,12 +261,12 @@ export default {
});
},
- enableSwitchEditingControl() {
- this.switchEditingControlDisabled = false;
+ trackContentEditorLoaded() {
+ this.track(CONTENT_EDITOR_LOADED_ACTION);
},
- disableSwitchEditingControl() {
- this.switchEditingControlDisabled = true;
+ submitFormWithShortcut() {
+ this.$refs.form.submit();
},
},
};
@@ -269,10 +274,12 @@ export default {
<template>
<gl-form
+ ref="form"
:action="formAction"
method="post"
class="wiki-form common-note-form gl-mt-3 js-quick-submit"
@submit="handleFormSubmit"
+ @input="isFormDirty = true"
>
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<input v-if="pageInfo.persisted" type="hidden" name="_method" value="put" />
@@ -329,74 +336,23 @@ export default {
<div class="row" data-testid="wiki-form-content-fieldset">
<div class="col-sm-12 row-sm-5">
<gl-form-group>
- <div v-if="isMarkdownFormat" class="gl-display-flex gl-justify-content-start gl-mb-3">
- <gl-segmented-control
- data-testid="toggle-editing-mode-button"
- data-qa-selector="editing_mode_button"
- class="gl-display-flex"
- :checked="editingMode"
- :options="$options.switchEditingControlOptions"
- :disabled="switchEditingControlDisabled"
- @input="setEditingMode"
- />
- </div>
- <local-storage-sync
- storage-key="gl-wiki-content-editor-enabled"
- :value="editingMode"
- @input="setEditingMode"
- />
- <markdown-field
- v-if="!isContentEditorActive"
- :markdown-preview-path="pageInfo.markdownPreviewPath"
- :can-attach-file="true"
- :enable-autocomplete="true"
- :textarea-value="content"
+ <markdown-editor
+ v-model="content"
+ :render-markdown-path="pageInfo.markdownPreviewPath"
:markdown-docs-path="pageInfo.markdownHelpPath"
:uploads-path="pageInfo.uploadsPath"
+ :enable-content-editor="isMarkdownFormat"
:enable-preview="isMarkdownFormat"
- class="bordered-box"
- >
- <template #textarea>
- <textarea
- id="wiki_content"
- ref="textarea"
- v-model="content"
- name="wiki[content]"
- class="note-textarea js-gfm-input js-autosize markdown-area"
- dir="auto"
- data-supports-quick-actions="false"
- data-qa-selector="wiki_content_textarea"
- :autofocus="pageInfo.persisted"
- :aria-label="$options.i18n.content.label"
- :placeholder="$options.i18n.content.placeholder"
- @input="handleContentChange"
- >
- </textarea>
- </template>
- </markdown-field>
- <div v-if="isContentEditorActive">
- <content-editor
- :render-markdown="renderMarkdown"
- :uploads-path="pageInfo.uploadsPath"
- :markdown="content"
- @initialized="trackContentEditorLoaded"
- @change="handleContentEditorChange"
- @loading="disableSwitchEditingControl"
- @loadingSuccess="enableSwitchEditingControl"
- @loadingError="enableSwitchEditingControl"
- />
- <input
- id="wiki_content"
- v-model.trim="content"
- type="hidden"
- name="wiki[content]"
- data-qa-selector="wiki_hidden_content"
- />
- </div>
-
- <div class="clearfix"></div>
- <div class="error-alert"></div>
-
+ :init-on-autofocus="pageInfo.persisted"
+ :form-field-placeholder="$options.i18n.content.placeholder"
+ :form-field-aria-label="$options.i18n.content.label"
+ form-field-id="wiki_content"
+ form-field-name="wiki[content]"
+ @contentEditor="notifyContentEditorActive"
+ @markdownField="notifyContentEditorInactive"
+ @keydown.ctrl.enter="submitFormShortcut"
+ @keydown.meta.enter="submitFormShortcut"
+ />
<div class="form-text gl-text-gray-600">
<gl-sprintf
v-if="displayWikiSpecificMarkdownHelp"
@@ -447,9 +403,14 @@ export default {
:disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
>
- <gl-button data-testid="wiki-cancel-button" :href="cancelFormPath" class="float-right">{{
- $options.i18n.cancel
- }}</gl-button>
+ <gl-button
+ data-testid="wiki-cancel-button"
+ :href="cancelFormPath"
+ class="float-right"
+ @click="isFormDirty = false"
+ >
+ {{ $options.i18n.cancel }}</gl-button
+ >
</div>
</gl-form>
</template>
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 9e0af426f6e..fb761725c43 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -1,7 +1,7 @@
import { select } from 'd3-selection';
import $ from 'jquery';
import { last } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import dateFormat from '~/lib/dateformat';
import axios from '~/lib/utils/axios_utils';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
@@ -151,7 +151,7 @@ export default class ActivityCalendar {
.select(container)
.append('svg')
.attr('width', width)
- .attr('height', 167)
+ .attr('height', 169)
.attr('class', 'contrib-calendar');
}
@@ -302,7 +302,7 @@ export default class ActivityCalendar {
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while retrieving calendar activity'),
}),
);
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
index ddc880db227..f35f9341fa1 100644
--- a/app/assets/javascripts/pdf/index.vue
+++ b/app/assets/javascripts/pdf/index.vue
@@ -1,9 +1,10 @@
<script>
-import pdfjsLib from 'pdfjs-dist/build/pdf';
-import workerSrc from 'pdfjs-dist/build/pdf.worker.min';
+import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist/legacy/build/pdf';
import Page from './page/index.vue';
+GlobalWorkerOptions.workerSrc = '/assets/webpack/pdfjs/pdf.worker.min.js';
+
export default {
components: { Page },
props: {
@@ -30,18 +31,16 @@ export default {
},
watch: { pdf: 'load' },
mounted() {
- pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
if (this.hasPDF) this.load();
},
methods: {
load() {
this.pages = [];
- return pdfjsLib
- .getDocument({
- url: this.document,
- cMapUrl: '/assets/webpack/cmaps/',
- cMapPacked: true,
- })
+ return getDocument({
+ url: this.document,
+ cMapUrl: '/assets/webpack/pdfjs/cmaps/',
+ cMapPacked: true,
+ })
.promise.then(this.renderPages)
.then((pages) => {
this.pages = pages;
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index 7e331bdd91d..6ee33902a01 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -1,4 +1,4 @@
-import createFlash from './flash';
+import { createAlert } from '~/flash';
import axios from './lib/utils/axios_utils';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
@@ -73,7 +73,7 @@ export default class PersistentUserCallout {
}
})
.catch(() => {
- createFlash({
+ createAlert({
message: __(
'An error occurred while dismissing the alert. Refresh the page and try again.',
),
@@ -94,7 +94,7 @@ export default class PersistentUserCallout {
window.location.assign(href);
})
.catch(() => {
- createFlash({
+ createAlert({
message: __(
'An error occurred while acknowledging the notification. Refresh the page and try again.',
),
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
index 3e87088e77e..7d2b9cd3d42 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
+++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
@@ -15,11 +15,20 @@ export default {
'Create a new %{codeStart}.gitlab-ci.yml%{codeEnd} file at the root of the repository to get started.',
),
btnText: __('Configure pipeline'),
+ externalCiNote: __("This project's pipeline configuration is located outside this repository"),
+ externalCiInstructions: __(
+ 'To edit the pipeline configuration, you must go to the project or external site that hosts the file.',
+ ),
},
inject: {
emptyStateIllustrationPath: {
default: '',
},
+ usesExternalConfig: {
+ default: false,
+ type: Boolean,
+ required: false,
+ },
},
methods: {
createEmptyConfigFile() {
@@ -33,22 +42,31 @@ export default {
<pipeline-editor-file-nav v-on="$listeners" />
<div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
<img :src="emptyStateIllustrationPath" />
- <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
- <p class="gl-mt-3">
- <gl-sprintf :message="$options.i18n.body">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
- </p>
- <gl-button
- variant="confirm"
- class="gl-mt-3"
- data-qa-selector="create_new_ci_button"
- @click="createEmptyConfigFile"
+ <div
+ v-if="usesExternalConfig"
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center"
>
- {{ $options.i18n.btnText }}
- </gl-button>
+ <h1 class="gl-font-size-h1">{{ $options.i18n.externalCiNote }}</h1>
+ <p class="gl-mt-3">{{ $options.i18n.externalCiInstructions }}</p>
+ </div>
+ <div v-else class="gl-display-flex gl-flex-direction-column gl-align-items-center">
+ <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
+ <p class="gl-mt-3">
+ <gl-sprintf :message="$options.i18n.body">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-button
+ variant="confirm"
+ class="gl-mt-3"
+ data-qa-selector="create_new_ci_button"
+ @click="createEmptyConfigFile"
+ >
+ {{ $options.i18n.btnText }}
+ </gl-button>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 13dad0b2459..6d91c339833 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import { EDITOR_APP_STATUS_LOADING } from './constants';
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
import getCurrentBranch from './graphql/queries/client/current_branch.query.graphql';
@@ -42,6 +43,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
projectNamespace,
simulatePipelineHelpPagePath,
totalBranches,
+ usesExternalConfig,
validateTabIllustrationPath,
ymlHelpPagePath,
} = el.dataset;
@@ -133,6 +135,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
projectNamespace,
simulatePipelineHelpPagePath,
totalBranches: parseInt(totalBranches, 10),
+ usesExternalConfig: parseBoolean(usesExternalConfig),
validateTabIllustrationPath,
ymlHelpPagePath,
},
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 548769eb214..ff848a973e3 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -40,7 +40,7 @@ export default {
PipelineEditorHome,
PipelineEditorMessages,
},
- inject: ['ciConfigPath', 'newMergeRequestPath', 'projectFullPath'],
+ inject: ['ciConfigPath', 'newMergeRequestPath', 'projectFullPath', 'usesExternalConfig'],
data() {
return {
ciConfigData: {},
@@ -397,7 +397,7 @@ export default {
<div class="gl-mt-4 gl-relative" data-qa-selector="pipeline_editor_app">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<pipeline-editor-empty-state
- v-else-if="showStartScreen"
+ v-else-if="showStartScreen || usesExternalConfig"
@createEmptyConfigFile="setNewEmptyCiConfigFile"
@refetchContent="refetchContent"
/>
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index 2d5c01a58b7..1972125ed56 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -162,7 +162,7 @@ export default {
</div>
</div>
<commit-section
- v-if="showCommitForm"
+ v-show="showCommitForm"
:ref="$options.commitSectionRef"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
diff --git a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue
index 529ec4897b4..cd7cb7f8393 100644
--- a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue
@@ -401,7 +401,7 @@ export default {
<div
v-for="(variable, index) in variables"
:key="variable.uniqueId"
- class="gl-mb-3 gl-ml-n3 gl-pb-2"
+ class="gl-mb-3 gl-pb-2"
data-testid="ci-variable-row"
data-qa-selector="ci_variable_row_container"
>
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index 529ec4897b4..a9af1181027 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -17,17 +17,11 @@ import {
import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import Vue from 'vue';
-import axios from '~/lib/utils/axios_utils';
-import { backOff } from '~/lib/utils/common_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__, __, n__ } from '~/locale';
-import {
- VARIABLE_TYPE,
- FILE_TYPE,
- CONFIG_VARIABLES_TIMEOUT,
- CC_VALIDATION_REQUIRED_ERROR,
-} from '../constants';
+import { VARIABLE_TYPE, FILE_TYPE, CC_VALIDATION_REQUIRED_ERROR } from '../constants';
+import createPipelineMutation from '../graphql/mutations/create_pipeline.mutation.graphql';
+import ciConfigVariablesQuery from '../graphql/queries/ci_config_variables.graphql';
import filterVariables from '../utils/filter_variables';
import RefsDropdown from './refs_dropdown.vue';
@@ -76,10 +70,6 @@ export default {
type: String,
required: true,
},
- configVariablesPath: {
- type: String,
- required: true,
- },
defaultBranch: {
type: String,
required: true,
@@ -97,6 +87,10 @@ export default {
required: false,
default: () => ({}),
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
refParam: {
type: String,
required: false,
@@ -116,19 +110,77 @@ export default {
return {
refValue: {
shortName: this.refParam,
+ // this is needed until we add support for ref type in url query strings
+ // ensure default branch is called with full ref on load
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
+ fullName: this.refParam === this.defaultBranch ? `refs/heads/${this.refParam}` : undefined,
},
form: {},
errorTitle: null,
error: null,
+ predefinedValueOptions: {},
warnings: [],
totalWarnings: 0,
isWarningDismissed: false,
- isLoading: false,
submitted: false,
ccAlertDismissed: false,
};
},
+ apollo: {
+ ciConfigVariables: {
+ query: ciConfigVariablesQuery,
+ // Skip when variables already cached in `form`
+ skip() {
+ return Object.keys(this.form).includes(this.refFullName);
+ },
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ ref: this.refQueryParam,
+ };
+ },
+ update({ project }) {
+ return project?.ciConfigVariables || [];
+ },
+ result({ data }) {
+ const predefinedVars = data?.project?.ciConfigVariables || [];
+ const params = {};
+ const descriptions = {};
+
+ predefinedVars.forEach(({ description, key, value, valueOptions }) => {
+ if (description) {
+ params[key] = value;
+ descriptions[key] = description;
+ this.predefinedValueOptions[key] = valueOptions;
+ }
+ });
+
+ Vue.set(this.form, this.refFullName, { descriptions, variables: [] });
+
+ // Add default variables from yml
+ this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
+
+ // Add/update variables, e.g. from query string
+ if (this.variableParams) {
+ this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
+ }
+
+ if (this.fileParams) {
+ this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
+ }
+
+ // Adds empty var at the end of the form
+ this.addEmptyVariable(this.refFullName);
+ },
+ error(error) {
+ Sentry.captureException(error);
+ },
+ },
+ },
computed: {
+ isLoading() {
+ return this.$apollo.queries.ciConfigVariables.loading;
+ },
overMaxWarningsLimit() {
return this.totalWarnings > this.maxWarnings;
},
@@ -147,6 +199,9 @@ export default {
refFullName() {
return this.refValue.fullName;
},
+ refQueryParam() {
+ return this.refFullName || this.refShortName;
+ },
variables() {
return this.form[this.refFullName]?.variables ?? [];
},
@@ -157,21 +212,6 @@ export default {
return this.error === CC_VALIDATION_REQUIRED_ERROR && !this.ccAlertDismissed;
},
},
- watch: {
- refValue() {
- this.loadConfigVariablesForm();
- },
- },
- created() {
- // this is needed until we add support for ref type in url query strings
- // ensure default branch is called with full ref on load
- // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
- if (this.refValue.shortName === this.defaultBranch) {
- this.refValue.fullName = `refs/heads/${this.refValue.shortName}`;
- }
-
- this.loadConfigVariablesForm();
- },
methods: {
addEmptyVariable(refValue) {
const { variables } = this.form[refValue];
@@ -204,132 +244,57 @@ export default {
});
}
},
- setVariableType(key, type) {
+ setVariableAttribute(key, attribute, value) {
const { variables } = this.form[this.refFullName];
const variable = variables.find((v) => v.key === key);
- variable.variable_type = type;
+ variable[attribute] = value;
},
setVariableParams(refValue, type, paramsObj) {
Object.entries(paramsObj).forEach(([key, value]) => {
this.setVariable(refValue, type, key, value);
});
},
+ shouldShowValuesDropdown(key) {
+ return this.predefinedValueOptions[key]?.length > 1;
+ },
removeVariable(index) {
this.variables.splice(index, 1);
},
canRemove(index) {
return index < this.variables.length - 1;
},
- loadConfigVariablesForm() {
- // Skip when variables already cached in `form`
- if (this.form[this.refFullName]) {
- return;
- }
-
- this.fetchConfigVariables(this.refFullName || this.refShortName)
- .then(({ descriptions, params }) => {
- Vue.set(this.form, this.refFullName, {
- variables: [],
- descriptions,
- });
-
- // Add default variables from yml
- this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
- })
- .catch(() => {
- Vue.set(this.form, this.refFullName, {
- variables: [],
- descriptions: {},
- });
- })
- .finally(() => {
- // Add/update variables, e.g. from query string
- if (this.variableParams) {
- this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
- }
- if (this.fileParams) {
- this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
- }
-
- // Adds empty var at the end of the form
- this.addEmptyVariable(this.refFullName);
- });
- },
- fetchConfigVariables(refValue) {
- this.isLoading = true;
-
- return backOff((next, stop) => {
- axios
- .get(this.configVariablesPath, {
- params: {
- sha: refValue,
- },
- })
- .then(({ data, status }) => {
- if (status === httpStatusCodes.NO_CONTENT) {
- next();
- } else {
- this.isLoading = false;
- stop(data);
- }
- })
- .catch((error) => {
- stop(error);
- });
- }, CONFIG_VARIABLES_TIMEOUT)
- .then((data) => {
- const params = {};
- const descriptions = {};
-
- Object.entries(data).forEach(([key, { value, description }]) => {
- if (description) {
- params[key] = value;
- descriptions[key] = description;
- }
- });
-
- return { params, descriptions };
- })
- .catch((error) => {
- this.isLoading = false;
-
- Sentry.captureException(error);
-
- return { params: {}, descriptions: {} };
- });
- },
- createPipeline() {
+ async createPipeline() {
this.submitted = true;
this.ccAlertDismissed = false;
- return axios
- .post(this.pipelinesPath, {
+ const { data } = await this.$apollo.mutate({
+ mutation: createPipelineMutation,
+ variables: {
+ endpoint: this.pipelinesPath,
// send shortName as fall back for query params
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
- ref: this.refValue.fullName || this.refShortName,
- variables_attributes: filterVariables(this.variables),
- })
- .then(({ data }) => {
- redirectTo(`${this.pipelinesPath}/${data.id}`);
- })
- .catch((err) => {
- // always re-enable submit button
- this.submitted = false;
+ ref: this.refQueryParam,
+ variablesAttributes: filterVariables(this.variables),
+ },
+ });
- const {
- errors = [],
- warnings = [],
- total_warnings: totalWarnings = 0,
- } = err.response.data;
- const [error] = errors;
+ const { id, errors, totalWarnings, warnings } = data.createPipeline;
- this.reportError({
- title: i18n.submitErrorTitle,
- error,
- warnings,
- totalWarnings,
- });
- });
+ if (id) {
+ redirectTo(`${this.pipelinesPath}/${id}`);
+ return;
+ }
+
+ // always re-enable submit button
+ this.submitted = false;
+ const [error] = errors;
+
+ this.reportError({
+ title: i18n.submitErrorTitle,
+ error,
+ warnings,
+ totalWarnings,
+ });
},
onRefsLoadingError(error) {
this.reportError({ title: i18n.refsLoadingErrorTitle });
@@ -401,7 +366,7 @@ export default {
<div
v-for="(variable, index) in variables"
:key="variable.uniqueId"
- class="gl-mb-3 gl-ml-n3 gl-pb-2"
+ class="gl-mb-3 gl-pb-2"
data-testid="ci-variable-row"
data-qa-selector="ci_variable_row_container"
>
@@ -416,7 +381,7 @@ export default {
<gl-dropdown-item
v-for="type in Object.keys($options.typeOptions)"
:key="type"
- @click="setVariableType(variable.key, type)"
+ @click="setVariableAttribute(variable.key, 'variable_type', type)"
>
{{ $options.typeOptions[type] }}
</gl-dropdown-item>
@@ -429,7 +394,24 @@ export default {
data-qa-selector="ci_variable_key_field"
@change="addEmptyVariable(refFullName)"
/>
+ <gl-dropdown
+ v-if="shouldShowValuesDropdown(variable.key)"
+ :text="variable.value"
+ :class="$options.formElementClasses"
+ class="gl-flex-grow-1 gl-mr-0!"
+ data-testid="pipeline-form-ci-variable-value-dropdown"
+ >
+ <gl-dropdown-item
+ v-for="value in predefinedValueOptions[variable.key]"
+ :key="value"
+ data-testid="pipeline-form-ci-variable-value-dropdown-items"
+ @click="setVariableAttribute(variable.key, 'value', value)"
+ >
+ {{ value }}
+ </gl-dropdown-item>
+ </gl-dropdown>
<gl-form-textarea
+ v-else
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mb-3"
diff --git a/app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql b/app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql
new file mode 100644
index 00000000000..a76e8f6b95b
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql
@@ -0,0 +1,9 @@
+mutation createPipeline($endpoint: String, $ref: String, $variablesAttributes: Array) {
+ createPipeline(endpoint: $endpoint, ref: $ref, variablesAttributes: $variablesAttributes)
+ @client {
+ id
+ errors
+ totalWarnings
+ warnings
+ }
+}
diff --git a/app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql b/app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql
new file mode 100644
index 00000000000..648cd8b66b5
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql
@@ -0,0 +1,11 @@
+query ciConfigVariables($fullPath: ID!, $ref: String!) {
+ project(fullPath: $fullPath) {
+ id
+ ciConfigVariables(sha: $ref) {
+ description
+ key
+ value
+ valueOptions
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipeline_new/graphql/resolvers.js b/app/assets/javascripts/pipeline_new/graphql/resolvers.js
new file mode 100644
index 00000000000..7b0f58e8cf9
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/graphql/resolvers.js
@@ -0,0 +1,29 @@
+import axios from '~/lib/utils/axios_utils';
+
+export const resolvers = {
+ Mutation: {
+ createPipeline: (_, { endpoint, ref, variablesAttributes }) => {
+ return axios
+ .post(endpoint, { ref, variables_attributes: variablesAttributes })
+ .then((response) => {
+ const { id } = response.data;
+ return {
+ id,
+ errors: [],
+ totalWarnings: 0,
+ warnings: [],
+ };
+ })
+ .catch((err) => {
+ const { errors = [], totalWarnings = 0, warnings = [] } = err.response.data;
+
+ return {
+ id: null,
+ errors,
+ totalWarnings,
+ warnings,
+ };
+ });
+ },
+ },
+};
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js
index e3f363f4ada..60b4c93d1d5 100644
--- a/app/assets/javascripts/pipeline_new/index.js
+++ b/app/assets/javascripts/pipeline_new/index.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import LegacyPipelineNewForm from './components/legacy_pipeline_new_form.vue';
import PipelineNewForm from './components/pipeline_new_form.vue';
+import { resolvers } from './graphql/resolvers';
const mountLegacyPipelineNewForm = (el) => {
const {
@@ -51,12 +54,12 @@ const mountPipelineNewForm = (el) => {
projectRefsEndpoint,
// props
- configVariablesPath,
defaultBranch,
fileParam,
maxWarnings,
pipelinesPath,
projectId,
+ projectPath,
refParam,
settingsLink,
varParam,
@@ -65,22 +68,27 @@ const mountPipelineNewForm = (el) => {
const variableParams = JSON.parse(varParam);
const fileParams = JSON.parse(fileParam);
- // TODO: add apolloProvider
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
return new Vue({
el,
+ apolloProvider,
provide: {
projectRefsEndpoint,
},
render(createElement) {
return createElement(PipelineNewForm, {
props: {
- configVariablesPath,
defaultBranch,
fileParams,
maxWarnings: Number(maxWarnings),
pipelinesPath,
projectId,
+ projectPath,
refParam,
settingsLink,
variableParams,
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue
new file mode 100644
index 00000000000..4a08a82275a
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline_schedule.mutation.graphql';
+import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
+import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
+
+export default {
+ i18n: {
+ schedulesFetchError: s__('PipelineSchedules|There was a problem fetching pipeline schedules.'),
+ scheduleDeleteError: s__(
+ 'PipelineSchedules|There was a problem deleting the pipeline schedule.',
+ ),
+ },
+ modal: {
+ id: 'delete-pipeline-schedule-modal',
+ deleteConfirmation: s__(
+ 'PipelineSchedules|Are you sure you want to delete this pipeline schedule?',
+ ),
+ actionPrimary: {
+ text: s__('PipelineSchedules|Delete pipeline schedule'),
+ attributes: [{ variant: 'danger' }],
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: [],
+ },
+ },
+ components: {
+ GlAlert,
+ GlLoadingIcon,
+ GlModal,
+ PipelineSchedulesTable,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ schedules: {
+ query: getPipelineSchedulesQuery,
+ variables() {
+ return {
+ projectPath: this.fullPath,
+ };
+ },
+ update({ project }) {
+ return project?.pipelineSchedules?.nodes || [];
+ },
+ error() {
+ this.reportError(this.$options.i18n.schedulesFetchError);
+ },
+ },
+ },
+ data() {
+ return {
+ schedules: [],
+ hasError: false,
+ errorMessage: '',
+ scheduleToDeleteId: null,
+ showModal: false,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.schedules.loading;
+ },
+ },
+ methods: {
+ reportError(error) {
+ this.hasError = true;
+ this.errorMessage = error;
+ },
+ showDeleteModal(id) {
+ this.showModal = true;
+ this.scheduleToDeleteId = id;
+ },
+ hideModal() {
+ this.showModal = false;
+ this.scheduleToDeleteId = null;
+ },
+ async deleteSchedule() {
+ try {
+ const {
+ data: {
+ pipelineScheduleDelete: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: deletePipelineScheduleMutation,
+ variables: { id: this.scheduleToDeleteId },
+ });
+
+ if (errors.length > 0) {
+ throw new Error();
+ } else {
+ this.$apollo.queries.schedules.refetch();
+ }
+ } catch {
+ this.reportError(this.$options.i18n.scheduleDeleteError);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert v-if="hasError" class="gl-mb-2" variant="danger" @dismiss="hasError = false">
+ {{ errorMessage }}
+ </gl-alert>
+
+ <gl-loading-icon v-if="isLoading" size="lg" />
+
+ <!-- Tabs will be addressed in #371989 -->
+
+ <template v-else>
+ <pipeline-schedules-table :schedules="schedules" @showDeleteModal="showDeleteModal" />
+
+ <gl-modal
+ :visible="showModal"
+ :title="$options.modal.actionPrimary.text"
+ :modal-id="$options.modal.id"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ size="sm"
+ @primary="deleteSchedule"
+ @hide="hideModal"
+ >
+ {{ $options.modal.deleteConfirmation }}
+ </gl-modal>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue
new file mode 100644
index 00000000000..6e24ac6b8d4
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -0,0 +1,18 @@
+<script>
+import { GlForm } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlForm,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form />
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
new file mode 100644
index 00000000000..76d118bf52d
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlButton, GlButtonGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export const i18n = {
+ playTooltip: s__('PipelineSchedules|Run pipeline schedule'),
+ editTooltip: s__('PipelineSchedules|Edit pipeline schedule'),
+ deleteTooltip: s__('PipelineSchedules|Delete pipeline schedule'),
+ takeOwnershipTooltip: s__('PipelineSchedules|Take ownership of pipeline schedule'),
+};
+
+export default {
+ i18n,
+ components: {
+ GlButton,
+ GlButtonGroup,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ schedule: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ canPlay() {
+ return this.schedule.userPermissions.playPipelineSchedule;
+ },
+ canTakeOwnership() {
+ return this.schedule.userPermissions.takeOwnershipPipelineSchedule;
+ },
+ canUpdate() {
+ return this.schedule.userPermissions.updatePipelineSchedule;
+ },
+ canRemove() {
+ return this.schedule.userPermissions.adminPipelineSchedule;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button-group>
+ <gl-button v-if="canPlay" v-gl-tooltip :title="$options.i18n.playTooltip" icon="play" />
+ <gl-button
+ v-if="canTakeOwnership"
+ v-gl-tooltip
+ :title="$options.i18n.takeOwnershipTooltip"
+ icon="user"
+ />
+ <gl-button v-if="canUpdate" v-gl-tooltip :title="$options.i18n.editTooltip" icon="pencil" />
+ <gl-button
+ v-if="canRemove"
+ v-gl-tooltip
+ :title="$options.i18n.deleteTooltip"
+ icon="remove"
+ variant="danger"
+ data-testid="delete-pipeline-schedule-btn"
+ @click="$emit('showDeleteModal', schedule.id)"
+ />
+ </gl-button-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
new file mode 100644
index 00000000000..216796b357c
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
@@ -0,0 +1,32 @@
+<script>
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+
+export default {
+ components: {
+ CiBadge,
+ },
+ props: {
+ schedule: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ hasPipeline() {
+ return this.schedule.lastPipeline;
+ },
+ lastPipelineStatus() {
+ return this.schedule?.lastPipeline?.detailedStatus;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <ci-badge v-if="hasPipeline" :status="lastPipelineStatus" class="gl-vertical-align-middle" />
+ <span v-else data-testid="pipeline-schedule-status-text">
+ {{ s__('PipelineSchedules|None') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue
new file mode 100644
index 00000000000..48d59bf6e7c
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue
@@ -0,0 +1,32 @@
+<script>
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ TimeAgoTooltip,
+ },
+ props: {
+ schedule: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ showTimeAgo() {
+ return this.schedule.active && this.schedule.nextRunAt;
+ },
+ realNextRunTime() {
+ return this.schedule.realNextRun;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <time-ago-tooltip v-if="showTimeAgo" :time="realNextRunTime" />
+ <span v-else data-testid="pipeline-schedule-inactive">
+ {{ s__('PipelineSchedules|Inactive') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue
new file mode 100644
index 00000000000..e7fa94eb7fc
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue
@@ -0,0 +1,29 @@
+<script>
+import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlAvatar,
+ GlAvatarLink,
+ },
+ props: {
+ schedule: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ owner() {
+ return this.schedule.owner;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-avatar-link :href="owner.webPath" :title="owner.name" class="gl-ml-3">
+ <gl-avatar :size="32" :src="owner.avatarUrl" />
+ </gl-avatar-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
new file mode 100644
index 00000000000..08efa794bcc
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ schedule: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ iconName() {
+ return this.schedule.forTag ? 'tag' : 'fork';
+ },
+ refPath() {
+ return this.schedule.refPath;
+ },
+ refDisplay() {
+ return this.schedule.refForDisplay;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-icon :name="iconName" />
+ <span v-if="refPath">
+ <gl-link :href="refPath" class="gl-text-gray-900">{{ refDisplay }}</gl-link>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue b/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue
new file mode 100644
index 00000000000..d54008b81b2
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlTableLite } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import PipelineScheduleActions from './cells/pipeline_schedule_actions.vue';
+import PipelineScheduleLastPipeline from './cells/pipeline_schedule_last_pipeline.vue';
+import PipelineScheduleNextRun from './cells/pipeline_schedule_next_run.vue';
+import PipelineScheduleOwner from './cells/pipeline_schedule_owner.vue';
+import PipelineScheduleTarget from './cells/pipeline_schedule_target.vue';
+
+export default {
+ fields: [
+ {
+ key: 'description',
+ label: s__('PipelineSchedules|Description'),
+ columnClass: 'gl-w-40p',
+ },
+ {
+ key: 'target',
+ label: s__('PipelineSchedules|Target'),
+ columnClass: 'gl-w-10p',
+ },
+ {
+ key: 'pipeline',
+ label: s__('PipelineSchedules|Last Pipeline'),
+ columnClass: 'gl-w-10p',
+ },
+ {
+ key: 'next',
+ label: s__('PipelineSchedules|Next Run'),
+ columnClass: 'gl-w-15p',
+ },
+ {
+ key: 'owner',
+ label: s__('PipelineSchedules|Owner'),
+ columnClass: 'gl-w-10p',
+ },
+ {
+ key: 'actions',
+ label: '',
+ columnClass: 'gl-w-15p',
+ },
+ ],
+ components: {
+ GlTableLite,
+ PipelineScheduleActions,
+ PipelineScheduleLastPipeline,
+ PipelineScheduleNextRun,
+ PipelineScheduleOwner,
+ PipelineScheduleTarget,
+ },
+ props: {
+ schedules: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-table-lite :fields="$options.fields" :items="schedules" stacked="md">
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+
+ <template #cell(description)="{ item }">
+ <span data-testid="pipeline-schedule-description">
+ {{ item.description }}
+ </span>
+ </template>
+
+ <template #cell(target)="{ item }">
+ <pipeline-schedule-target :schedule="item" />
+ </template>
+
+ <template #cell(pipeline)="{ item }">
+ <pipeline-schedule-last-pipeline :schedule="item" />
+ </template>
+
+ <template #cell(next)="{ item }">
+ <pipeline-schedule-next-run :schedule="item" />
+ </template>
+
+ <template #cell(owner)="{ item }">
+ <pipeline-schedule-owner :schedule="item" />
+ </template>
+
+ <template #cell(actions)="{ item }">
+ <pipeline-schedule-actions
+ :schedule="item"
+ @showDeleteModal="$emit('showDeleteModal', $event)"
+ />
+ </template>
+ </gl-table-lite>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql b/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql
new file mode 100644
index 00000000000..8aab0b3fbde
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql
@@ -0,0 +1,6 @@
+mutation deletePipelineSchedule($id: CiPipelineScheduleID!) {
+ pipelineScheduleDelete(input: { id: $id }) {
+ clientMutationId
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql b/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
new file mode 100644
index 00000000000..7d9d658b1b6
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
@@ -0,0 +1,40 @@
+query getPipelineSchedulesQuery($projectPath: ID!) {
+ project(fullPath: $projectPath) {
+ id
+ pipelineSchedules {
+ nodes {
+ id
+ description
+ forTag
+ refPath
+ refForDisplay
+ lastPipeline {
+ id
+ detailedStatus {
+ id
+ group
+ icon
+ label
+ text
+ detailsPath
+ }
+ }
+ active
+ nextRunAt
+ realNextRun
+ owner {
+ id
+ avatarUrl
+ name
+ webPath
+ }
+ userPermissions {
+ playPipelineSchedule
+ takeOwnershipPipelineSchedule
+ updatePipelineSchedule
+ adminPipelineSchedule
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js
new file mode 100644
index 00000000000..8f77e06c19a
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import PipelineSchedules from './components/pipeline_schedules.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const containerEl = document.querySelector('#pipeline-schedules-app');
+
+ if (!containerEl) {
+ return false;
+ }
+
+ const { fullPath } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ name: 'PipelineSchedulesRoot',
+ apolloProvider,
+ provide: {
+ fullPath,
+ },
+ render(createElement) {
+ return createElement(PipelineSchedules);
+ },
+ });
+};
diff --git a/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js
new file mode 100644
index 00000000000..d83417ab84a
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import PipelineSchedulesForm from './components/pipeline_schedules_form.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default (selector) => {
+ const containerEl = document.querySelector(selector);
+
+ if (!containerEl) {
+ return false;
+ }
+
+ const { fullPath } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ name: 'PipelineSchedulesFormRoot',
+ apolloProvider,
+ provide: {
+ fullPath,
+ },
+ render(createElement) {
+ return createElement(PipelineSchedulesForm);
+ },
+ });
+};
diff --git a/app/assets/javascripts/pipeline_wizard/components/widgets/list.vue b/app/assets/javascripts/pipeline_wizard/components/widgets/list.vue
index a5ce56daf07..bc62d6d4465 100644
--- a/app/assets/javascripts/pipeline_wizard/components/widgets/list.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/widgets/list.vue
@@ -176,7 +176,7 @@ export default {
category="secondary"
data-testid="remove-step-button"
icon="remove"
- @click="removeValue"
+ @click="() => removeValue(i)"
/>
</template>
</gl-form-input-group>
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
index 9e886fd7a48..605d40eddee 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import GetFailedJobsQuery from '../../graphql/queries/get_failed_jobs.query.graphql';
import { prepareFailedJobs } from './utils';
@@ -47,7 +47,7 @@ export default {
this.preparedFailedJobs = prepareFailedJobs(this.failedJobs, this.failedJobsSummary);
},
error() {
- createFlash({ message: s__('Jobs|There was a problem fetching the failed jobs.') });
+ createAlert({ message: s__('Jobs|There was a problem fetching the failed jobs.') });
},
},
},
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
index 0c6b8b9ed2b..18607bfae1c 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlLink, GlSafeHtmlDirective, GlTableLite } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import RetryFailedJobMutation from '../../graphql/mutations/retry_failed_job.mutation.graphql';
@@ -49,7 +49,7 @@ export default {
return job.retryable && job.userPermissions.updateBuild;
},
showErrorMessage() {
- createFlash({ message: s__('Job|There was a problem retrying the failed job.') });
+ createAlert({ message: s__('Job|There was a problem retrying the failed job.') });
},
},
};
diff --git a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
index 18e9ffa23cf..f1ad312dcaa 100644
--- a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlIntersectionObserver, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import produce from 'immer';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/jobs/components/table/event_hub';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
@@ -42,7 +42,7 @@ export default {
this.jobsPageInfo = data.project?.pipeline?.jobs?.pageInfo || {};
},
error() {
- createFlash({ message: __('An error occurred while fetching the pipelines jobs.') });
+ createAlert({ message: __('An error occurred while fetching the pipelines jobs.') });
},
},
},
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
index ca2537ca4f4..7ee5ec48f44 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { dasherize } from '~/lib/utils/text_utility';
@@ -81,7 +81,7 @@ export default {
reportToSentry('action_component', err);
- createFlash({
+ createAlert({
message: __('An error occurred while making the request.'),
});
});
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
index a68797a7235..f1c6c6633eb 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
@@ -14,7 +14,7 @@
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import eventHub from '../../event_hub';
@@ -94,7 +94,7 @@ export default {
this.$refs.dropdown.hide();
this.isLoading = false;
- createFlash({
+ createAlert({
message: __('Something went wrong on our end.'),
});
});
diff --git a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
index df59962569e..2a78636261b 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
@@ -56,7 +56,12 @@ export default {
<template>
<gl-tabs>
- <gl-tab ref="pipelineTab" :title="$options.i18n.tabs.pipelineTitle" data-testid="pipeline-tab">
+ <gl-tab
+ ref="pipelineTab"
+ :title="$options.i18n.tabs.pipelineTitle"
+ data-testid="pipeline-tab"
+ lazy
+ >
<pipeline-graph-wrapper />
</gl-tab>
<gl-tab
@@ -64,6 +69,7 @@ export default {
:title="$options.i18n.tabs.needsTitle"
:active="isActive($options.tabNames.needs)"
data-testid="dag-tab"
+ lazy
>
<dag />
</gl-tab>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
index 2d2f649f651..73a255f392b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
@@ -55,6 +55,9 @@ export default {
};
},
computed: {
+ hasArtifacts() {
+ return this.artifacts.length > 0;
+ },
filteredArtifacts() {
return this.searchQuery.length > 0
? fuzzaldrinPlus.filter(this.artifacts, this.searchQuery, { key: 'name' })
@@ -86,7 +89,9 @@ export default {
});
},
handleDropdownShown() {
- this.$refs.searchInput.focusInput();
+ if (this.hasArtifacts) {
+ this.$refs.searchInput.focusInput();
+ }
},
},
};
@@ -112,12 +117,12 @@ export default {
<gl-loading-icon v-else-if="isLoading" size="sm" />
- <gl-dropdown-item v-else-if="!artifacts.length" data-testid="artifacts-empty-message">
+ <gl-dropdown-item v-else-if="!hasArtifacts" data-testid="artifacts-empty-message">
{{ $options.i18n.emptyArtifactsMessage }}
</gl-dropdown-item>
<template #header>
- <gl-search-box-by-type v-if="artifacts.length" ref="searchInput" v-model.trim="searchQuery" />
+ <gl-search-box-by-type v-if="hasArtifacts" ref="searchInput" v-model.trim="searchQuery" />
</template>
<gl-dropdown-item
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index f9022be888a..30528ce8d17 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem, GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { isEqual } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO, VARIANT_WARNING } from '~/flash';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -249,7 +249,7 @@ export default {
this.updateContent(params);
- this.track('click_filter_tabs', { label: TRACKING_CATEGORIES.tabs });
+ this.track('click_filter_tabs', { label: TRACKING_CATEGORIES.tabs, property: scope });
},
successCallback(resp) {
// Because we are polling & the user is interacting verify if the response received
@@ -267,14 +267,14 @@ export default {
.postAction(endpoint)
.then(() => {
this.isResetCacheButtonLoading = false;
- createFlash({
+ createAlert({
message: s__('Pipelines|Project cache successfully reset.'),
- type: 'notice',
+ variant: VARIANT_INFO,
});
})
.catch(() => {
this.isResetCacheButtonLoading = false;
- createFlash({
+ createAlert({
message: s__('Pipelines|Something went wrong while cleaning runners cache.'),
});
});
@@ -301,9 +301,9 @@ export default {
}
if (!filter.type) {
- createFlash({
+ createAlert({
message: RAW_TEXT_WARNING,
- type: 'warning',
+ variant: VARIANT_WARNING,
});
}
});
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue
index 16a747f6165..f34b3f56c5b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue
@@ -1,6 +1,6 @@
<script>
import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { s__, __, sprintf } from '~/locale';
@@ -66,7 +66,7 @@ export default {
})
.catch(() => {
this.isLoading = false;
- createFlash({ message: __('An error occurred while making the request.') });
+ createAlert({ message: __('An error occurred while making the request.') });
});
},
isActionDisabled(action) {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
index 1db2898b72a..b57d0ac1fd7 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
@@ -2,7 +2,7 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
export default {
@@ -45,7 +45,7 @@ export default {
this.loading = false;
})
.catch((err) => {
- createFlash({
+ createAlert({
message: FETCH_BRANCH_ERROR_MESSAGE,
});
this.loading = false;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
index afcdd63b664..5846a1f6ed9 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
@@ -2,7 +2,7 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
export default {
@@ -38,7 +38,7 @@ export default {
this.loading = false;
})
.catch((err) => {
- createFlash({
+ createAlert({
message: FETCH_TAG_ERROR_MESSAGE,
});
this.loading = false;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
index 746cf238646..73f7d3f52c3 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
@@ -8,7 +8,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import {
ANY_TRIGGER_AUTHOR,
FETCH_AUTHOR_ERROR_MESSAGE,
@@ -61,7 +61,7 @@ export default {
this.loading = false;
})
.catch((err) => {
- createFlash({
+ createAlert({
message: FETCH_AUTHOR_ERROR_MESSAGE,
});
this.loading = false;
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
index e8e49cc652e..9602ca1ba88 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import createFlash, { createAlert } from '~/flash';
+import { createAlert } from '~/flash';
import { helpPagePath } from '~/helpers/help_page_helper';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -172,7 +172,7 @@ export default {
.postAction(endpoint)
.then(() => this.updateTable())
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while making the request.'),
}),
);
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 8bdf18da348..3744649e9d5 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__ } from '~/locale';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
@@ -24,7 +24,7 @@ export default async function initPipelineDetailsBundle() {
try {
createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading a section of this page.'),
});
}
@@ -37,7 +37,7 @@ export default async function initPipelineDetailsBundle() {
const appOptions = createAppOptions(SELECTORS.PIPELINE_TABS, apolloProvider);
createPipelineTabs(appOptions);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading a section of this page.'),
});
}
@@ -45,7 +45,7 @@ export default async function initPipelineDetailsBundle() {
try {
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading the pipeline.'),
});
}
@@ -53,7 +53,7 @@ export default async function initPipelineDetailsBundle() {
try {
createDagApp(apolloProvider);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading the Needs tab.'),
});
}
@@ -61,7 +61,7 @@ export default async function initPipelineDetailsBundle() {
try {
createTestDetails(SELECTORS.PIPELINE_TESTS);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading the Test Reports tab.'),
});
}
@@ -69,7 +69,7 @@ export default async function initPipelineDetailsBundle() {
try {
createPipelineJobsApp(SELECTORS.PIPELINE_JOBS);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading the Jobs tab.'),
});
}
@@ -77,7 +77,7 @@ export default async function initPipelineDetailsBundle() {
try {
createPipelineFailedJobsApp(SELECTORS.PIPELINE_FAILED_JOBS);
} catch {
- createFlash({
+ createAlert({
message: s__('Jobs|An error occurred while loading the Failed Jobs tab.'),
});
}
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
index b785fd1753c..c77b4813e33 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@@ -12,7 +12,7 @@ export const fetchSummary = ({ state, commit, dispatch }) => {
commit(types.SET_SUMMARY, data);
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__('TestReports|There was an error fetching the summary.'),
});
})
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
index 68ee063dda7..bff30acfe36 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@@ -21,7 +21,7 @@ export default {
if (errorMessage) {
state.errorMessage = errorMessage;
} else {
- createFlash({
+ createAlert({
message: s__('TestReports|There was an error fetching the test suite.'),
});
}
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index c99133fd251..b038b78088f 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -1,7 +1,7 @@
<script>
import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { escape } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, s__, sprintf } from '~/locale';
@@ -85,12 +85,12 @@ Please update your Git repository remotes as soon as possible.`),
return axios
.put(this.actionUrl, putData)
.then((result) => {
- createFlash({ message: result.data.message, type: 'notice' });
+ createAlert({ message: result.data.message, variant: VARIANT_INFO });
this.username = username;
this.isRequestPending = false;
})
.catch((error) => {
- createFlash({
+ createAlert({
message:
error?.response?.data?.message ||
s__('Profiles|An error occurred while updating your username, please try again.'),
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index 722f7d467a2..050b004f657 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -165,6 +165,7 @@ import { loadCSSFile } from '../lib/utils/css_utils';
setPreview() {
const filename = this.fileInput.val().replace(FILENAMEREGEX, '');
this.previewImage.attr('src', this.dataURL);
+ this.previewImage.attr('srcset', this.dataURL);
return this.filename.text(filename);
}
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index 7542f81a361..a33a20b49f6 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 createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_DANGER, VARIANT_INFO } from '~/flash';
import { INTEGRATION_VIEW_CONFIGS, i18n } from '../constants';
import IntegrationView from './integration_view.vue';
@@ -94,15 +94,15 @@ export default {
return;
}
updateClasses(this.bodyClasses, this.getSelectedTheme().css_class, this.selectedLayout);
- const { message = this.$options.i18n.defaultSuccess, type = FLASH_TYPES.NOTICE } =
+ const { message = this.$options.i18n.defaultSuccess, variant = VARIANT_INFO } =
customEvent?.detail?.[0] || {};
- createFlash({ message, type });
+ createAlert({ message, variant });
this.isSubmitEnabled = true;
},
handleError(customEvent) {
- const { message = this.$options.i18n.defaultError, type = FLASH_TYPES.ALERT } =
+ const { message = this.$options.i18n.defaultError, variant = VARIANT_DANGER } =
customEvent?.detail?.[0] || {};
- createFlash({ message, type });
+ createAlert({ message, variant });
this.isSubmitEnabled = true;
},
},
@@ -110,7 +110,7 @@ export default {
</script>
<template>
- <div class="row gl-mt-3 js-preferences-form">
+ <div class="row gl-mt-3 js-preferences-form js-search-settings-section">
<div v-if="integrationViews.length" class="col-sm-12">
<hr data-testid="profile-preferences-integrations-rule" />
</div>
@@ -131,9 +131,9 @@ export default {
:message-url="view.message_url"
:config="$options.integrationViewConfigs[view.name]"
/>
- </div>
- <div class="col-sm-12">
<hr />
+ </div>
+ <div class="col-sm-12 js-hide-when-nothing-matches-search">
<gl-button
category="primary"
variant="confirm"
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index af5beeb686c..93bc203d391 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -5,9 +5,6 @@ import axios from '~/lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import { parseRailsFormFields } from '~/lib/utils/forms';
import { Rails } from '~/lib/utils/rails_ujs';
-import TimezoneDropdown, {
- formatTimezone,
-} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
import UserProfileSetStatusWrapper from '~/set_status_modal/user_profile_set_status_wrapper.vue';
export default class Profile {
@@ -17,21 +14,12 @@ export default class Profile {
this.setRepoRadio();
this.bindEvents();
this.initAvatarGlCrop();
-
- this.$inputEl = $('#user_timezone');
-
- this.timezoneDropdown = new TimezoneDropdown({
- $inputEl: this.$inputEl,
- $dropdownEl: $('.js-timezone-dropdown'),
- displayFormat: (selectedItem) => formatTimezone(selectedItem),
- allowEmpty: true,
- });
}
initAvatarGlCrop() {
const cropOpts = {
filename: '.js-avatar-filename',
- previewImage: '.avatar-image .avatar',
+ previewImage: '.avatar-image .gl-avatar',
modalCrop: '.modal-profile-crop',
pickImageEl: '.js-choose-user-avatar-button',
uploadImageBtn: '.js-upload-user-avatar',
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 09acf98001c..705234537a8 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names */
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Api from './api';
import { loadCSSFile } from './lib/utils/css_utils';
import { s__ } from './locale';
@@ -67,7 +67,7 @@ const projectSelect = async () => {
},
projectsCallback,
).catch(() => {
- createFlash({
+ createAlert({
message: s__('ProjectSelect|Something went wrong while fetching projects'),
});
});
diff --git a/app/assets/javascripts/projects/commit/store/actions.js b/app/assets/javascripts/projects/commit/store/actions.js
index 2b25082eced..cfff93eac5a 100644
--- a/app/assets/javascripts/projects/commit/store/actions.js
+++ b/app/assets/javascripts/projects/commit/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { PROJECT_BRANCHES_ERROR } from '../constants';
import * as types from './mutation_types';
@@ -26,7 +26,7 @@ export const fetchBranches = ({ commit, dispatch, state }, query) => {
commit(types.RECEIVE_BRANCHES_SUCCESS, data.Branches?.length ? data.Branches : data);
})
.catch(() => {
- createFlash({ message: PROJECT_BRANCHES_ERROR });
+ createAlert({ message: PROJECT_BRANCHES_ERROR });
});
};
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
index 4505dd1f85c..2802e4a90b9 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import {
getQueryHeaders,
@@ -59,7 +59,7 @@ export default {
return project?.pipeline;
},
error() {
- createFlash({ message: this.$options.i18n.linkedPipelinesFetchError });
+ createAlert({ message: this.$options.i18n.linkedPipelinesFetchError });
},
},
pipelineStages: {
@@ -78,7 +78,7 @@ export default {
return project?.pipeline?.stages?.nodes || [];
},
error() {
- createFlash({ message: this.$options.i18n.stagesFetchError });
+ createAlert({ message: this.$options.i18n.stagesFetchError });
},
},
},
@@ -108,7 +108,7 @@ export default {
try {
this.formattedStages = formatStages(this.pipelineStages, this.stages);
} catch (error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.stageConversionError,
captureError: true,
error,
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
index 5a9d3129809..62b1209131c 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import {
getQueryHeaders,
toggleQueryPollingByVisibility,
@@ -44,7 +44,7 @@ export default {
return project?.pipeline?.detailedStatus || {};
},
error() {
- createFlash({ message: this.$options.PIPELINE_STATUS_FETCH_ERROR });
+ createAlert({ message: this.$options.PIPELINE_STATUS_FETCH_ERROR });
},
},
},
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index 795c293d14b..603fdfdf80a 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/browser';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -13,7 +13,7 @@ export default {
commit(types.COMMITS_AUTHORS, authors);
},
receiveAuthorsError() {
- createFlash({
+ createAlert({
message: __('An error occurred fetching the project authors.'),
});
},
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue
index 4ba7156b026..271694863e8 100644
--- a/app/assets/javascripts/projects/compare/components/app.vue
+++ b/app/assets/javascripts/projects/compare/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { joinPaths } from '~/lib/utils/url_utility';
import RevisionCard from './revision_card.vue';
@@ -9,6 +9,8 @@ export default {
components: {
RevisionCard,
GlButton,
+ GlDropdown,
+ GlDropdownItem,
},
props: {
projectCompareIndexPath: {
@@ -53,6 +55,10 @@ export default {
type: Array,
required: true,
},
+ straight: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -67,8 +73,27 @@ export default {
revision: this.paramsTo,
refsProjectPath: this.sourceProjectRefsPath,
},
+ isStraight: this.straight,
};
},
+ computed: {
+ straightModeDropdownItems() {
+ return [
+ {
+ modeType: 'off',
+ isEnabled: false,
+ content: '..',
+ testId: 'disableStraightModeButton',
+ },
+ {
+ modeType: 'on',
+ isEnabled: true,
+ content: '...',
+ testId: 'enableStraightModeButton',
+ },
+ ];
+ },
+ },
methods: {
onSubmit() {
this.$refs.form.submit();
@@ -85,6 +110,9 @@ export default {
onSwapRevision() {
[this.from, this.to] = [this.to, this.from]; // swaps 'from' and 'to'
},
+ setStraightMode(isStraight) {
+ this.isStraight = isStraight;
+ },
},
};
</script>
@@ -112,10 +140,22 @@ export default {
@selectRevision="onSelectRevision"
/>
<div
- class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-align-self-end gl-my-4 gl-md-my-0"
+ class="gl-display-flex gl-justify-content-center gl-align-items-center gl-align-self-end gl-my-3 gl-md-my-0 gl-pl-3 gl-pr-3"
data-testid="ellipsis"
>
- ...
+ <input :value="isStraight ? 'true' : 'false'" type="hidden" name="straight" />
+ <gl-dropdown data-testid="modeDropdown" :text="isStraight ? '...' : '..'" size="small">
+ <gl-dropdown-item
+ v-for="mode in straightModeDropdownItems"
+ :key="mode.modeType"
+ :is-check-item="true"
+ :is-checked="isStraight == mode.isEnabled"
+ :data-testid="mode.testId"
+ @click="setStraightMode(mode.isEnabled)"
+ >
+ <span class="dropdown-menu-inner-content"> {{ mode.content }} </span>
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
<revision-card
data-testid="targetRevisionCard"
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
index f0b8e73e528..10531e950f9 100644
--- a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
@@ -76,7 +76,7 @@ export default {
this.tags = data.Tags || [];
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__(
'CompareRevisions|There was an error while searching the branch/tag list. Please try again.',
),
@@ -97,7 +97,7 @@ export default {
this.tags = data.Tags || [];
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__(
'CompareRevisions|There was an error while loading the branch/tag list. Please try again.',
),
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
index 19cf4cda2be..1e1677e947c 100644
--- a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
@@ -1,6 +1,6 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
@@ -74,7 +74,7 @@ export default {
this.tags = data.Tags || [];
})
.catch(() => {
- createFlash({
+ createAlert({
message: `${s__(
'CompareRevisions|There was an error while updating the branch/tag list. Please try again.',
)}`,
diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js
index 074b8565c3c..284cee6d7f1 100644
--- a/app/assets/javascripts/projects/compare/index.js
+++ b/app/assets/javascripts/projects/compare/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import CompareApp from './components/app.vue';
export default function init() {
@@ -9,6 +10,7 @@ export default function init() {
targetProjectRefsPath,
paramsFrom,
paramsTo,
+ straight,
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
@@ -29,6 +31,7 @@ export default function init() {
targetProjectRefsPath,
paramsFrom,
paramsTo,
+ straight: parseBoolean(straight),
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
diff --git a/app/assets/javascripts/projects/project_find_file.js b/app/assets/javascripts/projects/project_find_file.js
index d295c06928f..71329c4f461 100644
--- a/app/assets/javascripts/projects/project_find_file.js
+++ b/app/assets/javascripts/projects/project_find_file.js
@@ -2,7 +2,7 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils';
@@ -89,7 +89,7 @@ export default class ProjectFindFile {
this.element.find('.files-slider tr.tree-item').eq(0).addClass('selected').focus();
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while loading filenames'),
}),
);
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index 060178a3cfb..335545c802a 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -1,7 +1,7 @@
/* eslint-disable no-underscore-dangle, class-methods-use-this */
import { escape, find, countBy } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { n__, s__, __, sprintf } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from './api/access_dropdown_api';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
@@ -326,12 +326,12 @@ export default class AccessDropdown {
);
})
.catch(() => {
- createFlash({ message: __('Failed to load groups, users and deploy keys.') });
+ createAlert({ message: __('Failed to load groups, users and deploy keys.') });
});
} else {
getDeployKeys(query)
.then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data)))
- .catch(() => createFlash({ message: __('Failed to load deploy keys.') }));
+ .catch(() => createAlert({ message: __('Failed to load deploy keys.') }));
}
}
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/branch_dropdown.vue
index 6ba2ef7da99..f2b1c749abc 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/branch_dropdown.vue
@@ -10,7 +10,7 @@ import {
import { createAlert } from '~/flash';
import { s__, sprintf } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
-import branchesQuery from '../queries/branches.query.graphql';
+import branchesQuery from '../../queries/branches.query.graphql';
export const i18n = {
fetchBranchesError: s__('BranchRules|An error occurred while fetching branches.'),
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/index.vue
index ad3eb7d2899..ad3eb7d2899 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/index.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/protections/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/index.vue
index bcc0f64d667..bcc0f64d667 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/protections/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/index.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/protections/merge_protections.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/merge_protections.vue
index 85f168af4a8..85f168af4a8 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/protections/merge_protections.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/merge_protections.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/protections/push_protections.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue
index 541923bb735..541923bb735 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/protections/push_protections.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
new file mode 100644
index 00000000000..264c2629433
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
@@ -0,0 +1,42 @@
+import { s__ } from '~/locale';
+
+export const I18N = {
+ manageProtectionsLinkTitle: s__('BranchRules|Manage in Protected Branches'),
+ targetBranch: s__('BranchRules|Target Branch'),
+ branchNameOrPattern: s__('BranchRules|Branch name or pattern'),
+ branch: s__('BranchRules|Target Branch'),
+ allBranches: s__('BranchRules|All branches'),
+ protectBranchTitle: s__('BranchRules|Protect branch'),
+ protectBranchDescription: s__(
+ 'BranchRules|Keep stable branches secure and force developers to use merge requests. %{linkStart}What are protected branches?%{linkEnd}',
+ ),
+ wildcardsHelpText: s__(
+ 'BranchRules|%{linkStart}Wildcards%{linkEnd} such as *-stable or production/ are supported',
+ ),
+ forcePushTitle: s__('BranchRules|Force push'),
+ allowForcePushDescription: s__(
+ 'BranchRules|All users with push access are allowed to force push.',
+ ),
+ disallowForcePushDescription: s__('BranchRules|Force push is not allowed.'),
+ approvalsTitle: s__('BranchRules|Approvals'),
+ manageApprovalsLinkTitle: s__('BranchRules|Manage in Merge Request Approvals'),
+ approvalsDescription: s__(
+ 'BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Lean more.%{linkEnd}',
+ ),
+ statusChecksTitle: s__('BranchRules|Status checks'),
+ allowedToPushHeader: s__('BranchRules|Allowed to push (%{total})'),
+ allowedToMergeHeader: s__('BranchRules|Allowed to merge (%{total})'),
+ approvalsHeader: s__('BranchRules|Required approvals (%{total})'),
+ noData: s__('BranchRules|No data to display'),
+};
+
+export const BRANCH_PARAM_NAME = 'branch';
+
+export const ALL_BRANCHES_WILDCARD = '*';
+
+export const WILDCARDS_HELP_PATH =
+ 'user/project/protected_branches#configure-multiple-protected-branches-by-using-a-wildcard';
+
+export const PROTECTED_BRANCHES_HELP_PATH = 'user/project/protected_branches';
+
+export const APPROVALS_HELP_PATH = 'user/project/merge_requests/approvals/index.md';
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
new file mode 100644
index 00000000000..318940478a8
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -0,0 +1,207 @@
+<script>
+import { GlSprintf, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { sprintf } from '~/locale';
+import { getParameterByName } from '~/lib/utils/url_utility';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import branchRulesQuery from '../../queries/branch_rules_details.query.graphql';
+import Protection from './protection.vue';
+import {
+ I18N,
+ ALL_BRANCHES_WILDCARD,
+ BRANCH_PARAM_NAME,
+ WILDCARDS_HELP_PATH,
+ PROTECTED_BRANCHES_HELP_PATH,
+ APPROVALS_HELP_PATH,
+} from './constants';
+
+const wildcardsHelpDocLink = helpPagePath(WILDCARDS_HELP_PATH);
+const protectedBranchesHelpDocLink = helpPagePath(PROTECTED_BRANCHES_HELP_PATH);
+const approvalsHelpDocLink = helpPagePath(APPROVALS_HELP_PATH);
+
+export default {
+ name: 'RuleView',
+ i18n: I18N,
+ wildcardsHelpDocLink,
+ protectedBranchesHelpDocLink,
+ approvalsHelpDocLink,
+ components: { Protection, GlSprintf, GlLink, GlLoadingIcon },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ protectedBranchesPath: {
+ default: '',
+ },
+ approvalRulesPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ project: {
+ query: branchRulesQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update({ project: { branchRules } }) {
+ this.branchProtection = branchRules.nodes.find(
+ (rule) => rule.name === this.branch,
+ )?.branchProtection;
+ },
+ },
+ },
+ data() {
+ return {
+ branch: getParameterByName(BRANCH_PARAM_NAME),
+ branchProtection: {
+ approvalRules: {},
+ },
+ };
+ },
+ computed: {
+ forcePushDescription() {
+ return this.branchProtection?.allowForcePush
+ ? this.$options.i18n.allowForcePushDescription
+ : this.$options.i18n.disallowForcePushDescription;
+ },
+ mergeAccessLevels() {
+ const { mergeAccessLevels } = this.branchProtection || {};
+ return this.getAccessLevels(mergeAccessLevels);
+ },
+ pushAccessLevels() {
+ const { pushAccessLevels } = this.branchProtection || {};
+ return this.getAccessLevels(pushAccessLevels);
+ },
+ allowedToMergeHeader() {
+ return sprintf(this.$options.i18n.allowedToMergeHeader, {
+ total: this.mergeAccessLevels.total,
+ });
+ },
+ allowedToPushHeader() {
+ return sprintf(this.$options.i18n.allowedToPushHeader, {
+ total: this.pushAccessLevels.total,
+ });
+ },
+ approvalsHeader() {
+ const total = this.approvals.reduce(
+ (sum, { approvalsRequired }) => sum + approvalsRequired,
+ 0,
+ );
+ return sprintf(this.$options.i18n.approvalsHeader, {
+ total,
+ });
+ },
+ allBranches() {
+ return this.branch === ALL_BRANCHES_WILDCARD;
+ },
+ allBranchesLabel() {
+ return this.$options.i18n.allBranches;
+ },
+ branchTitle() {
+ return this.allBranches
+ ? this.$options.i18n.targetBranch
+ : this.$options.i18n.branchNameOrPattern;
+ },
+ approvals() {
+ return this.branchProtection?.approvalRules?.nodes || [];
+ },
+ },
+ methods: {
+ getAccessLevels(accessLevels = {}) {
+ const total = accessLevels.edges?.length;
+ const accessLevelTypes = { total, users: [], groups: [], roles: [] };
+
+ accessLevels.edges?.forEach(({ node }) => {
+ if (node.user) {
+ const src = node.user.avatarUrl;
+ accessLevelTypes.users.push({ src, ...node.user });
+ } else if (node.group) {
+ accessLevelTypes.groups.push(node);
+ } else {
+ accessLevelTypes.roles.push(node);
+ }
+ });
+
+ return accessLevelTypes;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="$apollo.loading" />
+ <div v-else-if="!branchProtection">{{ $options.i18n.noData }}</div>
+ <div v-else>
+ <strong data-testid="branch-title">{{ branchTitle }}</strong>
+ <p v-if="!allBranches" class="gl-mb-3 gl-text-gray-400">
+ <gl-sprintf :message="$options.i18n.wildcardsHelpText">
+ <template #link="{ content }">
+ <gl-link :href="$options.wildcardsHelpDocLink">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <div v-if="allBranches" class="gl-mt-2" data-testid="branch">
+ {{ allBranchesLabel }}
+ </div>
+ <code v-else class="gl-mt-2" data-testid="branch">{{ branch }}</code>
+
+ <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.protectBranchTitle }}</h4>
+ <gl-sprintf :message="$options.i18n.protectBranchDescription">
+ <template #link="{ content }">
+ <gl-link :href="$options.protectedBranchesHelpDocLink">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+
+ <!-- Allowed to push -->
+ <protection
+ class="gl-mt-3"
+ :header="allowedToPushHeader"
+ :header-link-title="$options.i18n.manageProtectionsLinkTitle"
+ :header-link-href="protectedBranchesPath"
+ :roles="pushAccessLevels.roles"
+ :users="pushAccessLevels.users"
+ :groups="pushAccessLevels.groups"
+ />
+
+ <!-- Force push -->
+ <strong>{{ $options.i18n.forcePushTitle }}</strong>
+ <p>{{ forcePushDescription }}</p>
+
+ <!-- Allowed to merge -->
+ <protection
+ :header="allowedToMergeHeader"
+ :header-link-title="$options.i18n.manageProtectionsLinkTitle"
+ :header-link-href="protectedBranchesPath"
+ :roles="mergeAccessLevels.roles"
+ :users="mergeAccessLevels.users"
+ :groups="mergeAccessLevels.groups"
+ />
+
+ <!-- Approvals -->
+ <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.approvalsTitle }}</h4>
+ <gl-sprintf :message="$options.i18n.approvalsDescription">
+ <template #link="{ content }">
+ <gl-link :href="$options.approvalsHelpDocLink">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+
+ <protection
+ class="gl-mt-3"
+ :header="approvalsHeader"
+ :header-link-title="$options.i18n.manageApprovalsLinkTitle"
+ :header-link-href="approvalRulesPath"
+ :approvals="approvals"
+ />
+
+ <!-- Status checks -->
+ <!-- Follow-up: add status checks section (https://gitlab.com/gitlab-org/gitlab/-/issues/372362) -->
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue
new file mode 100644
index 00000000000..cfe2df0dbda
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue
@@ -0,0 +1,99 @@
+<script>
+import { GlCard, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import ProtectionRow from './protection_row.vue';
+
+export const i18n = {
+ rolesTitle: s__('BranchRules|Roles'),
+ usersTitle: s__('BranchRules|Users'),
+ groupsTitle: s__('BranchRules|Groups'),
+};
+
+export default {
+ name: 'ProtectionDetail',
+ i18n,
+ components: { GlCard, GlLink, ProtectionRow },
+ props: {
+ header: {
+ type: String,
+ required: true,
+ },
+ headerLinkTitle: {
+ type: String,
+ required: true,
+ },
+ headerLinkHref: {
+ type: String,
+ required: true,
+ },
+ roles: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ users: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ groups: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ approvals: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ showUsersDivider() {
+ return Boolean(this.roles.length);
+ },
+ showGroupsDivider() {
+ return Boolean(this.roles.length || this.users.length);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-card class="gl-mb-5" body-class="gl-py-0">
+ <template #header>
+ <div class="gl-display-flex gl-justify-content-space-between">
+ <strong>{{ header }}</strong>
+ <gl-link :href="headerLinkHref">{{ headerLinkTitle }}</gl-link>
+ </div>
+ </template>
+
+ <!-- Roles -->
+ <protection-row v-if="roles.length" :title="$options.i18n.rolesTitle" :access-levels="roles" />
+
+ <!-- Users -->
+ <protection-row
+ v-if="users.length"
+ :show-divider="showUsersDivider"
+ :users="users"
+ :title="$options.i18n.usersTitle"
+ />
+
+ <!-- Groups -->
+ <protection-row
+ v-if="groups.length"
+ :show-divider="showGroupsDivider"
+ :title="$options.i18n.groupsTitle"
+ :access-levels="groups"
+ />
+
+ <!-- Approvals -->
+ <protection-row
+ v-for="(approval, index) in approvals"
+ :key="approval.name"
+ :show-divider="index !== 0"
+ :title="approval.name"
+ :users="approval.eligibleApprovers.nodes"
+ :approvals-required="approval.approvalsRequired"
+ />
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
new file mode 100644
index 00000000000..28a1c09fa82
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlAvatarsInline, GlAvatar, GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
+import { n__ } from '~/locale';
+
+const AVATAR_TOOLTIP_MAX_CHARS = 100;
+export const MAX_VISIBLE_AVATARS = 4;
+export const AVATAR_SIZE = 32;
+
+export default {
+ name: 'ProtectionRow',
+ AVATAR_TOOLTIP_MAX_CHARS,
+ MAX_VISIBLE_AVATARS,
+ AVATAR_SIZE,
+ components: { GlAvatarsInline, GlAvatar, GlAvatarLink },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ accessLevels: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ showDivider: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ users: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ approvalsRequired: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ computed: {
+ avatarBadgeSrOnlyText() {
+ return n__(
+ '%d additional user',
+ '%d additional users',
+ this.users.length - this.$options.MAX_VISIBLE_AVATARS,
+ );
+ },
+ commaSeparateList() {
+ return this.accessLevels.length > 1;
+ },
+ approvalsRequiredTitle() {
+ return this.approvalsRequired
+ ? n__('%d approval required', '%d approvals required', this.approvalsRequired)
+ : null;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4"
+ :class="{ 'gl-border-t-solid': showDivider }"
+ >
+ <div class="gl-display-flex gl-w-half gl-justify-content-space-between">
+ <div class="gl-mr-7 gl-w-quarter">{{ title }}</div>
+
+ <gl-avatars-inline
+ v-if="users.length"
+ class="gl-w-quarter!"
+ :avatars="users"
+ :collapsed="true"
+ :max-visible="$options.MAX_VISIBLE_AVATARS"
+ :avatar-size="$options.AVATAR_SIZE"
+ badge-tooltip-prop="name"
+ :badge-tooltip-max-chars="$options.AVATAR_TOOLTIP_MAX_CHARS"
+ :badge-sr-only-text="avatarBadgeSrOnlyText"
+ >
+ <template #avatar="{ avatar }">
+ <gl-avatar-link
+ :key="avatar.username"
+ v-gl-tooltip
+ target="_blank"
+ :href="avatar.webUrl"
+ :title="avatar.name"
+ >
+ <gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="$options.AVATAR_SIZE" />
+ </gl-avatar-link>
+ </template>
+ </gl-avatars-inline>
+
+ <div
+ v-for="(item, index) in accessLevels"
+ :key="index"
+ data-testid="access-level"
+ class="gl-w-quarter"
+ >
+ <span v-if="commaSeparateList && index > 0" data-testid="comma-separator">,</span>
+ {{ item.accessLevelDescription }}
+ </div>
+
+ <div class="gl-ml-7 gl-w-quarter">{{ approvalsRequiredTitle }}</div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
index 8452542540e..07fd0a7080f 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
+++ b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import RuleEdit from './components/rule_edit.vue';
+import View from './components/view/index.vue';
export default function mountBranchRules(el) {
if (!el) {
@@ -14,13 +14,18 @@ export default function mountBranchRules(el) {
defaultClient: createDefaultClient(),
});
- const { projectPath } = el.dataset;
+ const { projectPath, protectedBranchesPath, approvalRulesPath } = el.dataset;
return new Vue({
el,
apolloProvider,
+ provide: {
+ projectPath,
+ protectedBranchesPath,
+ approvalRulesPath,
+ },
render(h) {
- return h(RuleEdit, { props: { projectPath } });
+ return h(View);
},
});
}
diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
new file mode 100644
index 00000000000..3ac165498a1
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
@@ -0,0 +1,50 @@
+query getBranchRulesDetails($projectPath: ID!) {
+ project(fullPath: $projectPath) {
+ id
+ branchRules {
+ nodes {
+ name
+ branchProtection {
+ allowForcePush
+ codeOwnerApprovalRequired
+ mergeAccessLevels {
+ edges {
+ node {
+ accessLevel
+ accessLevelDescription
+ group {
+ id
+ avatarUrl
+ }
+ user {
+ id
+ name
+ avatarUrl
+ webUrl
+ }
+ }
+ }
+ }
+ pushAccessLevels {
+ edges {
+ node {
+ accessLevel
+ accessLevelDescription
+ group {
+ id
+ avatarUrl
+ }
+ user {
+ id
+ name
+ avatarUrl
+ webUrl
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index 2209172c06d..cc47496971d 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -9,7 +9,7 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__, n__ } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api';
import { LEVEL_TYPES, ACCESS_LEVELS } from '../constants';
@@ -163,7 +163,7 @@ export default {
this.setSelected({ initial });
})
.catch(() =>
- createFlash({ message: __('Failed to load groups, users and deploy keys.') }),
+ createAlert({ message: __('Failed to load groups, users and deploy keys.') }),
)
.finally(() => {
this.initialLoading = false;
@@ -175,7 +175,7 @@ export default {
this.consolidateData(deployKeysResponse.data);
this.setSelected({ initial });
})
- .catch(() => createFlash({ message: __('Failed to load deploy keys.') }))
+ .catch(() => createAlert({ message: __('Failed to load deploy keys.') }))
.finally(() => {
this.initialLoading = false;
this.loading = false;
diff --git a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
new file mode 100644
index 00000000000..fee2f591216
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
@@ -0,0 +1,38 @@
+<script>
+import RefSelector from '~/ref/components/ref_selector.vue';
+import { REF_TYPE_BRANCHES } from '~/ref/constants';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ RefSelector,
+ },
+ props: {
+ persistedDefaultBranch: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: true,
+ },
+ },
+ refTypes: [REF_TYPE_BRANCHES],
+ i18n: {
+ dropdownHeader: __('Select default branch'),
+ searchPlaceholder: __('Search branch'),
+ },
+};
+</script>
+<template>
+ <ref-selector
+ :value="persistedDefaultBranch"
+ class="gl-w-full"
+ :project-id="projectId"
+ :enabled-ref-types="$options.refTypes"
+ :translations="$options.i18n"
+ name="project[default_branch]"
+ data-testid="default-branch-dropdown"
+ data-qa-selector="default_branch_dropdown"
+ />
+</template>
diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
index c13753da00b..55420c9c732 100644
--- a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
+++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
@@ -1,13 +1,14 @@
<script>
-import { GlFormGroup } from '@gitlab/ui';
-import produce from 'immer';
+import { GlFormGroup, GlAlert } from '@gitlab/ui';
+import { debounce } from 'lodash';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
+import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { getTransferLocations } from '~/api/projects_api';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import searchNamespacesWhereUserCanTransferProjects from '../graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql';
-
-const GROUPS_PER_PAGE = 25;
+import { s__, __ } from '~/locale';
+import currentUserNamespace from '../graphql/queries/current_user_namespace.query.graphql';
export default {
name: 'TransferProjectForm',
@@ -15,7 +16,15 @@ export default {
GlFormGroup,
NamespaceSelect,
ConfirmDanger,
+ GlAlert,
+ },
+ i18n: {
+ errorMessage: s__(
+ 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.',
+ ),
+ alertDismissAlert: __('Dismiss'),
},
+ inject: ['projectId'],
props: {
confirmationPhrase: {
type: String,
@@ -26,93 +35,131 @@ export default {
required: true,
},
},
- apollo: {
- currentUser: {
- query: searchNamespacesWhereUserCanTransferProjects,
- debounce: DEBOUNCE_DELAY,
- variables() {
- return {
- search: this.searchTerm,
- after: null,
- first: GROUPS_PER_PAGE,
- };
- },
- result() {
- this.isLoadingMoreGroups = false;
- this.isSearchLoading = false;
- },
- },
- },
data() {
return {
- currentUser: {},
+ userNamespaces: [],
+ groupNamespaces: [],
+ initialNamespacesLoaded: false,
selectedNamespace: null,
- isLoadingMoreGroups: false,
+ hasError: false,
+ isLoading: false,
isSearchLoading: false,
searchTerm: '',
+ page: 1,
+ totalPages: 1,
};
},
computed: {
hasSelectedNamespace() {
return Boolean(this.selectedNamespace?.id);
},
- groupNamespaces() {
- return this.currentUser.groups?.nodes?.map(this.formatNamespace) || [];
- },
- userNamespaces() {
- const { namespace } = this.currentUser;
-
- return namespace ? [this.formatNamespace(namespace)] : [];
- },
hasNextPageOfGroups() {
- return this.currentUser.groups?.pageInfo?.hasNextPage || false;
+ return this.page < this.totalPages;
},
},
methods: {
+ async handleShow() {
+ if (this.initialNamespacesLoaded) {
+ return;
+ }
+
+ this.isLoading = true;
+
+ [this.groupNamespaces, this.userNamespaces] = await Promise.all([
+ this.getGroupNamespaces(),
+ this.getUserNamespaces(),
+ ]);
+
+ this.isLoading = false;
+ this.initialNamespacesLoaded = true;
+ },
handleSelect(selectedNamespace) {
this.selectedNamespace = selectedNamespace;
this.$emit('selectNamespace', selectedNamespace.id);
},
- handleLoadMoreGroups() {
- this.isLoadingMoreGroups = true;
+ async getGroupNamespaces() {
+ try {
+ const { data: groupNamespaces, headers } = await getTransferLocations(this.projectId, {
+ page: this.page,
+ search: this.searchTerm,
+ });
+
+ const { totalPages } = parseIntPagination(normalizeHeaders(headers));
+ this.totalPages = totalPages;
- this.$apollo.queries.currentUser.fetchMore({
- variables: {
- after: this.currentUser.groups.pageInfo.endCursor,
- first: GROUPS_PER_PAGE,
- },
- updateQuery(
- previousResult,
+ return groupNamespaces.map(({ id, full_name: humanName }) => ({
+ id,
+ humanName,
+ }));
+ } catch (error) {
+ this.hasError = true;
+
+ return [];
+ }
+ },
+ async getUserNamespaces() {
+ try {
+ const {
+ data: {
+ currentUser: { namespace },
+ },
+ } = await this.$apollo.query({
+ query: currentUserNamespace,
+ });
+
+ if (!namespace) {
+ return [];
+ }
+
+ return [
{
- fetchMoreResult: {
- currentUser: { groups: newGroups },
- },
+ id: getIdFromGraphQLId(namespace.id),
+ humanName: namespace.fullName,
},
- ) {
- const previousGroups = previousResult.currentUser.groups;
+ ];
+ } catch (error) {
+ this.hasError = true;
- return produce(previousResult, (draftData) => {
- draftData.currentUser.groups.nodes = [...previousGroups.nodes, ...newGroups.nodes];
- draftData.currentUser.groups.pageInfo = newGroups.pageInfo;
- });
- },
- });
+ return [];
+ }
},
- handleSearch(searchTerm) {
+ async handleLoadMoreGroups() {
+ this.isLoading = true;
+ this.page += 1;
+
+ const groupNamespaces = await this.getGroupNamespaces();
+ this.groupNamespaces.push(...groupNamespaces);
+
+ this.isLoading = false;
+ },
+ debouncedSearch: debounce(async function debouncedSearch() {
this.isSearchLoading = true;
+
+ this.groupNamespaces = await this.getGroupNamespaces();
+
+ this.isSearchLoading = false;
+ }, DEBOUNCE_DELAY),
+ handleSearch(searchTerm) {
this.searchTerm = searchTerm;
+ this.page = 1;
+
+ this.debouncedSearch();
},
- formatNamespace({ id, fullName }) {
- return {
- id: getIdFromGraphQLId(id),
- humanName: fullName,
- };
+ handleAlertDismiss() {
+ this.hasError = false;
},
},
};
</script>
<template>
<div>
+ <gl-alert
+ v-if="hasError"
+ variant="danger"
+ :dismiss-label="$options.i18n.alertDismissLabel"
+ @dismiss="handleAlertDismiss"
+ >{{ $options.i18n.errorMessage }}</gl-alert
+ >
<gl-form-group>
<namespace-select
data-testid="transfer-project-namespace"
@@ -121,12 +168,13 @@ export default {
:user-namespaces="userNamespaces"
:selected-namespace="selectedNamespace"
:has-next-page-of-groups="hasNextPageOfGroups"
- :is-loading-more-groups="isLoadingMoreGroups"
+ :is-loading="isLoading"
:is-search-loading="isSearchLoading"
:should-filter-namespaces="false"
@select="handleSelect"
@load-more-groups="handleLoadMoreGroups"
@search="handleSearch"
+ @show="handleShow"
/>
</gl-form-group>
<confirm-danger
diff --git a/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql b/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql
new file mode 100644
index 00000000000..7ae6ee1428b
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql
@@ -0,0 +1,9 @@
+query currentUserNamespace {
+ currentUser {
+ id
+ namespace {
+ id
+ fullName
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql b/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql
deleted file mode 100644
index d4bcb8c869c..00000000000
--- a/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql
+++ /dev/null
@@ -1,24 +0,0 @@
-#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-
-query searchNamespacesWhereUserCanTransferProjects(
- $search: String = ""
- $after: String = ""
- $first: Int = null
-) {
- currentUser {
- id
- groups(permissionScope: TRANSFER_PROJECTS, search: $search, after: $after, first: $first) {
- nodes {
- id
- fullName
- }
- pageInfo {
- ...PageInfo
- }
- }
- namespace {
- id
- fullName
- }
- }
-}
diff --git a/app/assets/javascripts/projects/settings/init_transfer_project_form.js b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
index bc1aff640d2..89c158a9ba8 100644
--- a/app/assets/javascripts/projects/settings/init_transfer_project_form.js
+++ b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
@@ -12,6 +12,7 @@ export default () => {
Vue.use(VueApollo);
const {
+ projectId,
targetFormId = null,
targetHiddenInputId = null,
buttonText: confirmButtonText = '',
@@ -26,6 +27,7 @@ export default () => {
}),
provide: {
confirmDangerMessage,
+ projectId,
},
render(createElement) {
return createElement(TransferProjectForm, {
diff --git a/app/assets/javascripts/projects/settings/mount_default_branch_selector.js b/app/assets/javascripts/projects/settings/mount_default_branch_selector.js
new file mode 100644
index 00000000000..611561e38f2
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/mount_default_branch_selector.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import DefaultBranchSelector from './components/default_branch_selector.vue';
+
+export default (el) => {
+ if (!el) {
+ return null;
+ }
+
+ const { projectId, defaultBranch } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(DefaultBranchSelector, {
+ props: {
+ persistedDefaultBranch: defaultBranch,
+ projectId,
+ },
+ });
+ },
+ });
+};
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 e8eaf0a70b2..94793a535cc 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -1,6 +1,6 @@
<script>
import { s__ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import branchRulesQuery from './graphql/queries/branch_rules.query.graphql';
import BranchRule from './components/branch_rule.vue';
@@ -31,14 +31,13 @@ export default {
return data.project?.branchRules?.nodes || [];
},
error() {
- createFlash({ message: this.$options.i18n.queryError });
+ createAlert({ message: this.$options.i18n.queryError });
},
},
},
- props: {
+ inject: {
projectPath: {
- type: String,
- required: true,
+ default: '',
},
},
data() {
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index 68750318029..2b88f8561d7 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -1,10 +1,11 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
export const i18n = {
defaultLabel: s__('BranchRules|default'),
protectedLabel: s__('BranchRules|protected'),
+ detailsButtonLabel: s__('BranchRules|Details'),
};
export default {
@@ -12,6 +13,12 @@ export default {
i18n,
components: {
GlBadge,
+ GlButton,
+ },
+ inject: {
+ branchRulesPath: {
+ default: '',
+ },
},
props: {
name: {
@@ -38,24 +45,30 @@ export default {
hasApprovalDetails() {
return this.approvalDetails && this.approvalDetails.length;
},
+ detailsPath() {
+ return `${this.branchRulesPath}?branch=${this.name}`;
+ },
},
};
</script>
<template>
- <div class="gl-border-b gl-pt-5 gl-pb-5">
- <strong class="gl-font-monospace">{{ name }}</strong>
+ <div class="gl-border-b gl-pt-5 gl-pb-5 gl-display-flex gl-justify-content-space-between">
+ <div>
+ <strong class="gl-font-monospace">{{ name }}</strong>
- <gl-badge v-if="isDefault" variant="info" size="sm" class="gl-ml-2">{{
- $options.i18n.defaultLabel
- }}</gl-badge>
+ <gl-badge v-if="isDefault" variant="info" size="sm" class="gl-ml-2">{{
+ $options.i18n.defaultLabel
+ }}</gl-badge>
- <gl-badge v-if="isProtected" variant="success" size="sm" class="gl-ml-2">{{
- $options.i18n.protectedLabel
- }}</gl-badge>
+ <gl-badge v-if="isProtected" variant="success" size="sm" class="gl-ml-2">{{
+ $options.i18n.protectedLabel
+ }}</gl-badge>
- <ul v-if="hasApprovalDetails" class="gl-pl-6 gl-mt-2 gl-mb-0 gl-text-gray-500">
- <li v-for="(detail, index) in approvalDetails" :key="index">{{ detail }}</li>
- </ul>
+ <ul v-if="hasApprovalDetails" class="gl-pl-6 gl-mt-2 gl-mb-0 gl-text-gray-500">
+ <li v-for="(detail, index) in approvalDetails" :key="index">{{ detail }}</li>
+ </ul>
+ </div>
+ <gl-button :href="detailsPath"> {{ $options.i18n.detailsButtonLabel }}</gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
index 35322e2e466..042be089e09 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
@@ -12,17 +12,17 @@ const apolloProvider = new VueApollo({
export default function mountBranchRules(el) {
if (!el) return null;
- const { projectPath } = el.dataset;
+ const { projectPath, branchRulesPath } = el.dataset;
return new Vue({
el,
apolloProvider,
+ provide: {
+ projectPath,
+ branchRulesPath,
+ },
render(createElement) {
- return createElement(BranchRulesApp, {
- props: {
- projectPath,
- },
- });
+ return createElement(BranchRulesApp);
},
});
}
diff --git a/app/assets/javascripts/projects/star.js b/app/assets/javascripts/projects/star.js
index e063064663b..55c3d68cd11 100644
--- a/app/assets/javascripts/projects/star.js
+++ b/app/assets/javascripts/projects/star.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils';
import { __, s__ } from '~/locale';
@@ -7,7 +7,7 @@ export default class Star {
constructor(containerSelector = '.project-home-panel') {
const container = document.querySelector(containerSelector);
const starToggle = container.querySelector('.toggle-star');
- starToggle.addEventListener('click', function toggleStarClickCallback() {
+ starToggle?.addEventListener('click', function toggleStarClickCallback() {
const starSpan = starToggle.querySelector('span');
const starIcon = starToggle.querySelector('svg');
const iconClasses = Array.from(starIcon.classList.values());
@@ -34,7 +34,7 @@ export default class Star {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Star toggle failed. Try again later.'),
}),
);
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index 6b14ebadacc..9f9b6424125 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __, s__, sprintf } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -57,7 +57,7 @@ export default {
group: 'notfound',
};
this.isLoading = false;
- createFlash({
+ createAlert({
message: __('Something went wrong on our end'),
});
},
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 16eb5c3de32..120f75d4f0c 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import CreateItemDropdown from '~/create_item_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import AccessorUtilities from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -74,7 +74,7 @@ export default class ProtectedBranchCreate {
$allowedToPush.length
);
- this.$form.find('input[type="submit"]').attr('disabled', toggle);
+ this.$form.find('button[type="submit"]').attr('disabled', toggle);
}
static getProtectedBranches(term, callback) {
@@ -130,7 +130,7 @@ export default class ProtectedBranchCreate {
window.location.reload();
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Failed to protect the branch'),
}),
);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 15e706e38c6..1693d869b54 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,5 +1,5 @@
import { find } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import AccessDropdown from '~/projects/settings/access_dropdown';
@@ -74,7 +74,7 @@ export default class ProtectedBranchEdit {
})
.then(callback)
.catch(() => {
- createFlash({ message: __('Failed to update branch!') });
+ createAlert({ message: __('Failed to update branch!') });
});
}
@@ -141,7 +141,7 @@ export default class ProtectedBranchEdit {
.catch(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
- createFlash({ message: __('Failed to update branch!') });
+ createAlert({ message: __('Failed to update branch!') });
});
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index 1fe9a753e1e..40c52eba99e 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '../lib/utils/axios_utils';
import { FAILED_TO_UPDATE_TAG_MESSAGE } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
@@ -49,7 +49,7 @@ export default class ProtectedTagEdit {
this.$allowedToCreateDropdownButton.enable();
window.scrollTo({ top: 0, behavior: 'smooth' });
- createFlash({
+ createAlert({
message: FAILED_TO_UPDATE_TAG_MESSAGE,
});
});
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index 1343ad8246c..b75958e2ced 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -29,6 +29,7 @@ export default {
GlLoadingIcon,
RefResultsSection,
},
+ inheritAttrs: false,
props: {
enabledRefTypes: {
type: Array,
@@ -70,6 +71,15 @@ export default {
required: false,
default: true,
},
+
+ /* Underlying form field name for scenarios where ref_selector
+ * is used as part of submitting an HTML form
+ */
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -213,89 +223,103 @@ export default {
</script>
<template>
- <gl-dropdown
- :header-text="i18n.dropdownHeader"
- :toggle-class="toggleButtonClass"
- :text="buttonText"
- class="ref-selector"
- v-bind="$attrs"
- v-on="$listeners"
- @shown="focusSearchBox"
- >
- <template #header>
- <gl-search-box-by-type
- ref="searchBox"
- v-model.trim="query"
- :placeholder="i18n.searchPlaceholder"
- autocomplete="off"
- @input="onSearchBoxInput"
- @keydown.enter.prevent="onSearchBoxEnter"
- />
- </template>
+ <div>
+ <gl-dropdown
+ :header-text="i18n.dropdownHeader"
+ :toggle-class="toggleButtonClass"
+ :text="buttonText"
+ class="ref-selector gl-w-full"
+ v-bind="$attrs"
+ v-on="$listeners"
+ @shown="focusSearchBox"
+ >
+ <template #header>
+ <gl-search-box-by-type
+ ref="searchBox"
+ v-model.trim="query"
+ :placeholder="i18n.searchPlaceholder"
+ autocomplete="off"
+ data-qa-selector="ref_selector_searchbox"
+ @input="onSearchBoxInput"
+ @keydown.enter.prevent="onSearchBoxEnter"
+ />
+ </template>
- <gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" />
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" />
- <div v-else-if="showNoResults" class="gl-text-center gl-mx-3 gl-py-3" data-testid="no-results">
- <gl-sprintf v-if="lastQuery" :message="i18n.noResultsWithQuery">
- <template #query>
- <b class="gl-word-break-all">{{ lastQuery }}</b>
- </template>
- </gl-sprintf>
+ <div
+ v-else-if="showNoResults"
+ class="gl-text-center gl-mx-3 gl-py-3"
+ data-testid="no-results"
+ >
+ <gl-sprintf v-if="lastQuery" :message="i18n.noResultsWithQuery">
+ <template #query>
+ <b class="gl-word-break-all">{{ lastQuery }}</b>
+ </template>
+ </gl-sprintf>
- <span v-else>{{ i18n.noResults }}</span>
- </div>
+ <span v-else>{{ i18n.noResults }}</span>
+ </div>
- <template v-else>
- <template v-if="showBranchesSection">
- <ref-results-section
- :section-title="i18n.branches"
- :total-count="matches.branches.totalCount"
- :items="matches.branches.list"
- :selected-ref="selectedRef"
- :error="matches.branches.error"
- :error-message="i18n.branchesErrorMessage"
- :show-header="showSectionHeaders"
- data-testid="branches-section"
- data-qa-selector="branches_section"
- @selected="selectRef($event)"
- />
+ <template v-else>
+ <template v-if="showBranchesSection">
+ <ref-results-section
+ :section-title="i18n.branches"
+ :total-count="matches.branches.totalCount"
+ :items="matches.branches.list"
+ :selected-ref="selectedRef"
+ :error="matches.branches.error"
+ :error-message="i18n.branchesErrorMessage"
+ :show-header="showSectionHeaders"
+ data-testid="branches-section"
+ data-qa-selector="branches_section"
+ @selected="selectRef($event)"
+ />
- <gl-dropdown-divider v-if="showTagsSection || showCommitsSection" />
- </template>
+ <gl-dropdown-divider v-if="showTagsSection || showCommitsSection" />
+ </template>
- <template v-if="showTagsSection">
- <ref-results-section
- :section-title="i18n.tags"
- :total-count="matches.tags.totalCount"
- :items="matches.tags.list"
- :selected-ref="selectedRef"
- :error="matches.tags.error"
- :error-message="i18n.tagsErrorMessage"
- :show-header="showSectionHeaders"
- data-testid="tags-section"
- @selected="selectRef($event)"
- />
+ <template v-if="showTagsSection">
+ <ref-results-section
+ :section-title="i18n.tags"
+ :total-count="matches.tags.totalCount"
+ :items="matches.tags.list"
+ :selected-ref="selectedRef"
+ :error="matches.tags.error"
+ :error-message="i18n.tagsErrorMessage"
+ :show-header="showSectionHeaders"
+ data-testid="tags-section"
+ @selected="selectRef($event)"
+ />
- <gl-dropdown-divider v-if="showCommitsSection" />
- </template>
+ <gl-dropdown-divider v-if="showCommitsSection" />
+ </template>
- <template v-if="showCommitsSection">
- <ref-results-section
- :section-title="i18n.commits"
- :total-count="matches.commits.totalCount"
- :items="matches.commits.list"
- :selected-ref="selectedRef"
- :error="matches.commits.error"
- :error-message="i18n.commitsErrorMessage"
- :show-header="showSectionHeaders"
- data-testid="commits-section"
- @selected="selectRef($event)"
- />
+ <template v-if="showCommitsSection">
+ <ref-results-section
+ :section-title="i18n.commits"
+ :total-count="matches.commits.totalCount"
+ :items="matches.commits.list"
+ :selected-ref="selectedRef"
+ :error="matches.commits.error"
+ :error-message="i18n.commitsErrorMessage"
+ :show-header="showSectionHeaders"
+ data-testid="commits-section"
+ @selected="selectRef($event)"
+ />
+ </template>
</template>
- </template>
- <template #footer>
- <slot name="footer" v-bind="footerSlotProps"></slot>
- </template>
- </gl-dropdown>
+ <template #footer>
+ <slot name="footer" v-bind="footerSlotProps"></slot>
+ </template>
+ </gl-dropdown>
+ <input
+ v-if="name"
+ data-testid="selected-ref-form-field"
+ type="hidden"
+ :value="selectedRef"
+ :name="name"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue
index 53f2dbbbbd7..1ab41ee2f0a 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_block.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -233,6 +233,7 @@ export default {
<div
v-if="isFormVisible"
class="js-add-related-issues-form-area card-body bordered-box bg-white"
+ :class="{ 'gl-mb-5': shouldShowTokenBody }"
>
<add-issuable-form
:show-categorized-issues="showCategorizedIssues"
@@ -253,7 +254,7 @@ export default {
</div>
<template v-if="shouldShowTokenBody">
<related-issues-list
- v-for="category in categorisedIssues"
+ v-for="(category, index) in categorisedIssues"
:key="category.linkType"
:list-link-type="category.linkType"
:heading="$options.linkedIssueTypesTextMap[category.linkType]"
@@ -263,6 +264,7 @@ export default {
:issuable-type="issuableType"
:path-id-separator="pathIdSeparator"
:related-issues="category.issues"
+ :class="{ 'gl-mt-5': index > 0 }"
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
@saveReorder="$emit('saveReorder', $event)"
/>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue
index ae40232df6f..38e1d6e9d4f 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_root.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue
@@ -23,7 +23,7 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways:
and hide the `AddIssuableForm` area.
*/
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import {
@@ -141,11 +141,11 @@ export default {
})
.catch((res) => {
if (res && res.status !== 404) {
- createFlash({ message: relatedIssuesRemoveErrorMap[this.issuableType] });
+ createAlert({ message: relatedIssuesRemoveErrorMap[this.issuableType] });
}
});
} else {
- createFlash({ message: pathIndeterminateErrorMap[this.issuableType] });
+ createAlert({ message: pathIndeterminateErrorMap[this.issuableType] });
}
},
onToggleAddRelatedIssuesForm() {
@@ -174,7 +174,7 @@ export default {
if (response && response.data && response.data.message) {
errorMessage = response.data.message;
}
- createFlash({ message: errorMessage });
+ createAlert({ message: errorMessage });
})
.finally(() => {
this.isSubmitting = false;
@@ -195,7 +195,7 @@ export default {
})
.catch(() => {
this.store.setRelatedIssues([]);
- createFlash({ message: __('An error occurred while fetching issues.') });
+ createAlert({ message: __('An error occurred while fetching issues.') });
})
.finally(() => {
this.isFetching = false;
@@ -216,7 +216,7 @@ export default {
}
})
.catch(() => {
- createFlash({ message: __('An error occurred while reordering issues.') });
+ createAlert({ message: __('An error occurred while reordering issues.') });
});
}
},
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index d63a83d1a08..6dc8240e680 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { historyPushState } from '~/lib/utils/common_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { setUrlParams, getParameterByName } from '~/lib/utils/url_utility';
@@ -71,7 +71,7 @@ export default {
error(error) {
this.fullRequestError = true;
- createFlash({
+ createAlert({
message: this.$options.i18n.errorMessage,
captureError: true,
error,
diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue
index fdb0f99b735..7147cfa01c8 100644
--- a/app/assets/javascripts/releases/components/app_show.vue
+++ b/app/assets/javascripts/releases/components/app_show.vue
@@ -1,5 +1,5 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import oneReleaseQuery from '../graphql/queries/one_release.query.graphql';
import { convertGraphQLRelease } from '../util';
@@ -51,7 +51,7 @@ export default {
},
methods: {
showFlash(error) {
- createFlash({
+ createAlert({
message: s__('Release|Something went wrong while getting the release details.'),
captureError: true,
error,
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 08b727dcca0..2ddab5dddea 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -1,8 +1,15 @@
<script>
-import { GlFormGroup, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import {
+ GlCollapse,
+ GlLink,
+ GlFormGroup,
+ GlFormTextarea,
+ GlDropdownItem,
+ GlSprintf,
+} from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapState, mapActions, mapGetters } from 'vuex';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_TAGS } from '~/ref/constants';
import FormFieldContainer from './form_field_container.vue';
@@ -10,7 +17,10 @@ import FormFieldContainer from './form_field_container.vue';
export default {
name: 'TagFieldNew',
components: {
+ GlCollapse,
GlFormGroup,
+ GlFormTextarea,
+ GlLink,
RefSelector,
FormFieldContainer,
GlDropdownItem,
@@ -41,6 +51,14 @@ export default {
this.updateShowCreateFrom(false);
},
},
+ tagMessage: {
+ get() {
+ return this.release.tagMessage;
+ },
+ set(tagMessage) {
+ this.updateReleaseTagMessage(tagMessage);
+ },
+ },
createFromModel: {
get() {
return this.createFrom;
@@ -70,6 +88,7 @@ export default {
methods: {
...mapActions('editNew', [
'updateReleaseTagName',
+ 'updateReleaseTagMessage',
'updateCreateFrom',
'fetchTagNotes',
'updateShowCreateFrom',
@@ -113,9 +132,20 @@ export default {
noRefSelected: __('No source selected'),
searchPlaceholder: __('Search branches, tags, and commits'),
dropdownHeader: __('Select source'),
+ label: __('Create from'),
+ description: __('Existing branch name, tag, or commit SHA'),
+ },
+ annotatedTag: {
+ label: s__('CreateGitTag|Set tag message'),
+ description: s__(
+ 'CreateGitTag|Add a message to the tag. Leaving this blank creates a %{linkStart}lightweight tag%{linkEnd}.',
+ ),
},
},
+ tagMessageId: uniqueId('tag-message-'),
+
tagNameEnabledRefTypes: [REF_TYPE_TAGS],
+ gitTagDocsLink: 'https://git-scm.com/book/en/v2/Git-Basics-Tagging/',
};
</script>
<template>
@@ -156,23 +186,45 @@ export default {
</ref-selector>
</form-field-container>
</gl-form-group>
- <gl-form-group
- v-if="showCreateFrom"
- :label="__('Create from')"
- :label-for="createFromSelectorId"
- data-testid="create-from-field"
- >
- <form-field-container>
- <ref-selector
- :id="createFromSelectorId"
- v-model="createFromModel"
- :project-id="projectId"
- :translations="$options.translations.createFrom"
- />
- </form-field-container>
- <template #description>
- {{ __('Existing branch name, tag, or commit SHA') }}
- </template>
- </gl-form-group>
+ <gl-collapse :visible="showCreateFrom">
+ <div class="gl-pl-6 gl-border-l-1 gl-border-l-solid gl-border-gray-300">
+ <gl-form-group
+ v-if="showCreateFrom"
+ :label="$options.translations.createFrom.label"
+ :label-for="createFromSelectorId"
+ data-testid="create-from-field"
+ >
+ <form-field-container>
+ <ref-selector
+ :id="createFromSelectorId"
+ v-model="createFromModel"
+ :project-id="projectId"
+ :translations="$options.translations.createFrom"
+ />
+ </form-field-container>
+ <template #description>{{ $options.translations.createFrom.description }}</template>
+ </gl-form-group>
+ <gl-form-group
+ v-if="showCreateFrom"
+ :label="$options.translations.annotatedTag.label"
+ :label-for="$options.tagMessageId"
+ data-testid="annotated-tag-message-field"
+ >
+ <gl-form-textarea :id="$options.tagMessageId" v-model="tagMessage" />
+ <template #description>
+ <gl-sprintf :message="$options.translations.annotatedTag.description">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.gitTagDocsLink"
+ rel="noopener noreferrer"
+ target="_blank"
+ >{{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-form-group>
+ </div>
+ </gl-collapse>
</div>
</template>
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 669e5928143..42ceed81c00 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -1,5 +1,5 @@
import { getTag } from '~/rest_api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
@@ -48,7 +48,7 @@ export const fetchRelease = async ({ commit, state }) => {
commit(types.RECEIVE_RELEASE_SUCCESS, release);
} catch (error) {
commit(types.RECEIVE_RELEASE_ERROR, error);
- createFlash({
+ createAlert({
message: s__('Release|Something went wrong while getting the release details.'),
});
}
@@ -57,6 +57,9 @@ export const fetchRelease = async ({ commit, state }) => {
export const updateReleaseTagName = ({ commit }, tagName) =>
commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
+export const updateReleaseTagMessage = ({ commit }, tagMessage) =>
+ commit(types.UPDATE_RELEASE_TAG_MESSAGE, tagMessage);
+
export const updateCreateFrom = ({ commit }, createFrom) =>
commit(types.UPDATE_CREATE_FROM, createFrom);
@@ -133,11 +136,11 @@ export const createRelease = async ({ commit, dispatch, getters }) => {
} catch (error) {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
if (error instanceof GraphQLError) {
- createFlash({
+ createAlert({
message: error.message,
});
} else {
- createFlash({
+ createAlert({
message: s__('Release|Something went wrong while creating a new release.'),
});
}
@@ -219,7 +222,7 @@ export const updateRelease = async ({ commit, dispatch, state, getters }) => {
dispatch('receiveSaveReleaseSuccess', state.release._links.self);
} catch (error) {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
- createFlash({
+ createAlert({
message: s__('Release|Something went wrong while saving the release details.'),
});
}
@@ -233,7 +236,7 @@ export const fetchTagNotes = ({ commit, state }, tagName) => {
commit(types.RECEIVE_TAG_NOTES_SUCCESS, data);
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__('Release|Unable to fetch the tag notes.'),
});
@@ -266,7 +269,7 @@ export const deleteRelease = ({ commit, getters, dispatch, state }) => {
})
.catch((error) => {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
- createFlash({
+ createAlert({
message: s__('Release|Something went wrong while deleting the 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 ccca9ca8250..0d77095d099 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
@@ -145,6 +145,7 @@ export const releaseCreateMutatationVariables = (state, getters) => {
input: {
...getters.releaseUpdateMutatationVariables.input,
ref: state.createFrom,
+ tagMessage: state.release.tagMessage,
assets: {
links: getters.releaseLinksToCreate.map(({ name, url, linkType }) => ({
name: name.trim(),
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
index 0ef017f4eb4..e52eccd6a21 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
@@ -5,6 +5,7 @@ export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
export const UPDATE_RELEASE_TAG_NAME = 'UPDATE_RELEASE_TAG_NAME';
+export const UPDATE_RELEASE_TAG_MESSAGE = 'UPDATE_RELEASE_TAG_MESSAGE';
export const UPDATE_CREATE_FROM = 'UPDATE_CREATE_FROM';
export const UPDATE_SHOW_CREATE_FROM = 'UPDATE_SHOW_CREATE_FROM';
export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
index 34361f84a5a..f80e75501c9 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
@@ -10,6 +10,7 @@ export default {
[types.INITIALIZE_EMPTY_RELEASE](state) {
state.release = {
tagName: state.tagName,
+ tagMessage: '',
name: '',
description: '',
milestones: [],
@@ -40,6 +41,9 @@ export default {
[types.UPDATE_RELEASE_TAG_NAME](state, tagName) {
state.release.tagName = tagName;
},
+ [types.UPDATE_RELEASE_TAG_MESSAGE](state, tagMessage) {
+ state.release.tagMessage = tagMessage;
+ },
[types.UPDATE_CREATE_FROM](state, createFrom) {
state.createFrom = createFrom;
},
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 11a2f9df59b..3112becfa9e 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/state.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
@@ -37,7 +37,7 @@ export default ({
* When creating a new release, this is the default from the URL
*/
tagName,
- showCreateFrom: !tagName,
+ showCreateFrom: false,
defaultBranch,
createFrom: defaultBranch,
diff --git a/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue b/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue
deleted file mode 100644
index 05ab5c2cc90..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-import { GlBadge, GlLink } from '@gitlab/ui';
-
-export default {
- name: 'AccessibilityIssueBody',
- components: {
- GlBadge,
- GlLink,
- },
- props: {
- issue: {
- type: Object,
- required: true,
- },
- isNew: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- parsedTECHSCode() {
- /*
- * In issue code looks like "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
- * or "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent"
- *
- * The TECHS code is the "G18", "G168", "H91", etc. from the code which is used for the documentation.
- * Here we simply split the string on `.` and get the code in the 5th position
- */
- return this.issue.code?.split('.')[4];
- },
- learnMoreUrl() {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return `https://www.w3.org/TR/WCAG20-TECHS/${this.parsedTECHSCode || 'Overview'}.html`;
- },
- },
-};
-</script>
-<template>
- <div class="report-block-list-issue-description gl-mt-2 gl-mb-2">
- <div ref="accessibility-issue-description" class="report-block-list-issue-description-text">
- <gl-badge v-if="isNew" class="gl-mr-2" variant="danger">{{
- s__('AccessibilityReport|New')
- }}</gl-badge>
- <div>
- {{
- sprintf(
- s__(
- 'AccessibilityReport|The accessibility scanning found an error of the following type: %{code}',
- ),
- { code: issue.code },
- )
- }}
- <gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{
- s__('AccessibilityReport|Learn more')
- }}</gl-link>
- </div>
- {{ sprintf(s__('AccessibilityReport|Message: %{message}'), { message: issue.message }) }}
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue b/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
deleted file mode 100644
index 99cdeae545e..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import { mapActions, mapGetters } from 'vuex';
-import { componentNames } from '~/reports/components/issue_body';
-import IssuesList from '~/reports/components/issues_list.vue';
-import ReportSection from '~/reports/components/report_section.vue';
-import createStore from './store';
-
-export default {
- name: 'GroupedAccessibilityReportsApp',
- store: createStore(),
- components: {
- ReportSection,
- IssuesList,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- },
- componentNames,
- computed: {
- ...mapGetters([
- 'summaryStatus',
- 'groupedSummaryText',
- 'shouldRenderIssuesList',
- 'unresolvedIssues',
- 'resolvedIssues',
- 'newIssues',
- ]),
- },
- created() {
- this.setEndpoint(this.endpoint);
-
- this.fetchReport();
- },
- methods: {
- ...mapActions(['fetchReport', 'setEndpoint']),
- },
-};
-</script>
-<template>
- <report-section
- :status="summaryStatus"
- :success-text="groupedSummaryText"
- :loading-text="groupedSummaryText"
- :error-text="groupedSummaryText"
- :has-issues="shouldRenderIssuesList"
- track-action="users_expanding_testing_accessibility_report"
- class="mr-widget-section grouped-security-reports mr-report"
- >
- <template #body>
- <div class="mr-widget-grouped-section report-block">
- <issues-list
- v-if="shouldRenderIssuesList"
- :unresolved-issues="unresolvedIssues"
- :new-issues="newIssues"
- :resolved-issues="resolvedIssues"
- :component="$options.componentNames.AccessibilityIssueBody"
- class="report-block-group-list"
- />
- </div>
- </template>
- </report-section>
-</template>
diff --git a/app/assets/javascripts/reports/accessibility_report/store/actions.js b/app/assets/javascripts/reports/accessibility_report/store/actions.js
deleted file mode 100644
index e0142a35291..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/actions.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import Visibility from 'visibilityjs';
-import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-import Poll from '~/lib/utils/poll';
-import * as types from './mutation_types';
-
-let eTagPoll;
-
-export const clearEtagPoll = () => {
- eTagPoll = null;
-};
-
-export const stopPolling = () => {
- if (eTagPoll) eTagPoll.stop();
-};
-
-export const restartPolling = () => {
- if (eTagPoll) eTagPoll.restart();
-};
-
-export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
-
-/**
- * We need to poll the report endpoint while they are being parsed in the Backend.
- * This can take up to one minute.
- *
- * Poll.js will handle etag response.
- * While http status code is 204, it means it's parsing, and we'll keep polling
- * When http status code is 200, it means parsing is done, we can show the results & stop polling
- * When http status code is 500, it means parsing went wrong and we stop polling
- */
-export const fetchReport = ({ state, dispatch, commit }) => {
- commit(types.REQUEST_REPORT);
-
- eTagPoll = new Poll({
- resource: {
- getReport(endpoint) {
- return axios.get(endpoint);
- },
- },
- data: state.endpoint,
- method: 'getReport',
- successCallback: ({ status, data }) => dispatch('receiveReportSuccess', { status, data }),
- errorCallback: () => dispatch('receiveReportError'),
- });
-
- if (!Visibility.hidden()) {
- eTagPoll.makeRequest();
- } else {
- axios
- .get(state.endpoint)
- .then(({ status, data }) => dispatch('receiveReportSuccess', { status, data }))
- .catch(() => dispatch('receiveReportError'));
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden() && state.isLoading) {
- dispatch('restartPolling');
- } else {
- dispatch('stopPolling');
- }
- });
-};
-
-export const receiveReportSuccess = ({ commit, dispatch }, { status, data }) => {
- if (status === httpStatusCodes.OK) {
- commit(types.RECEIVE_REPORT_SUCCESS, data);
- // Stop polling since we have the information already parsed and it won't be changing
- dispatch('stopPolling');
- }
-};
-
-export const receiveReportError = ({ commit, dispatch }) => {
- commit(types.RECEIVE_REPORT_ERROR);
- dispatch('stopPolling');
-};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/getters.js b/app/assets/javascripts/reports/accessibility_report/store/getters.js
deleted file mode 100644
index 20506b1bfd1..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/getters.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { s__, n__ } from '~/locale';
-import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants';
-
-export const groupedSummaryText = (state) => {
- if (state.isLoading) {
- return s__('Reports|Accessibility scanning results are being parsed');
- }
-
- if (state.hasError) {
- return s__('Reports|Accessibility scanning failed loading results');
- }
-
- const numberOfResults = state.report?.summary?.errored || 0;
- if (numberOfResults === 0) {
- return s__('Reports|Accessibility scanning detected no issues for the source branch only');
- }
-
- return n__(
- 'Reports|Accessibility scanning detected %d issue for the source branch only',
- 'Reports|Accessibility scanning detected %d issues for the source branch only',
- numberOfResults,
- );
-};
-
-export const summaryStatus = (state) => {
- if (state.isLoading) {
- return LOADING;
- }
-
- if (state.hasError || state.status === STATUS_FAILED) {
- return ERROR;
- }
-
- return SUCCESS;
-};
-
-export const shouldRenderIssuesList = (state) =>
- Object.values(state.report).some((x) => Array.isArray(x) && x.length > 0);
-
-// We could just map state, but we're going to iterate in the future
-// to add notes and warnings to these issue lists, so I'm going to
-// keep these as getters
-export const unresolvedIssues = (state) => state.report.existing_errors;
-export const resolvedIssues = (state) => state.report.resolved_errors;
-export const newIssues = (state) => state.report.new_errors;
diff --git a/app/assets/javascripts/reports/accessibility_report/store/index.js b/app/assets/javascripts/reports/accessibility_report/store/index.js
deleted file mode 100644
index 5bfcd69edec..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-Vue.use(Vuex);
-
-export const getStoreConfig = (initialState) => ({
- actions,
- getters,
- mutations,
- state: state(initialState),
-});
-
-export default (initialState) => new Vuex.Store(getStoreConfig(initialState));
diff --git a/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js b/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js
deleted file mode 100644
index 22e2330e1ea..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const SET_ENDPOINT = 'SET_ENDPOINT';
-
-export const REQUEST_REPORT = 'REQUEST_REPORT';
-export const RECEIVE_REPORT_SUCCESS = 'RECEIVE_REPORT_SUCCESS';
-export const RECEIVE_REPORT_ERROR = 'RECEIVE_REPORT_ERROR';
diff --git a/app/assets/javascripts/reports/accessibility_report/store/mutations.js b/app/assets/javascripts/reports/accessibility_report/store/mutations.js
deleted file mode 100644
index 20d3e5be9a3..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/mutations.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.SET_ENDPOINT](state, endpoint) {
- state.endpoint = endpoint;
- },
- [types.REQUEST_REPORT](state) {
- state.isLoading = true;
- },
- [types.RECEIVE_REPORT_SUCCESS](state, report) {
- state.hasError = false;
- state.isLoading = false;
- state.report = report;
- },
- [types.RECEIVE_REPORT_ERROR](state) {
- state.isLoading = false;
- state.hasError = true;
- state.report = {};
- },
-};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/state.js b/app/assets/javascripts/reports/accessibility_report/store/state.js
deleted file mode 100644
index 2a4cefea5e6..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/state.js
+++ /dev/null
@@ -1,28 +0,0 @@
-export default (initialState = {}) => ({
- endpoint: initialState.endpoint || '',
-
- isLoading: initialState.isLoading || false,
- hasError: initialState.hasError || false,
-
- /**
- * Report will have the following format:
- * {
- * status: {String},
- * summary: {
- * total: {Number},
- * resolved: {Number},
- * errored: {Number},
- * },
- * existing_errors: {Array.<Object>},
- * existing_notes: {Array.<Object>},
- * existing_warnings: {Array.<Object>},
- * new_errors: {Array.<Object>},
- * new_notes: {Array.<Object>},
- * new_warnings: {Array.<Object>},
- * resolved_errors: {Array.<Object>},
- * resolved_notes: {Array.<Object>},
- * resolved_warnings: {Array.<Object>},
- * }
- */
- report: initialState.report || {},
-});
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js
index 04e72809e62..a76a6f45c07 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/reports/components/issue_body.js
@@ -1,14 +1,11 @@
import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
export const components = {
- AccessibilityIssueBody: () =>
- import('../accessibility_report/components/accessibility_issue_body.vue'),
CodequalityIssueBody: () => import('../codequality_report/components/codequality_issue_body.vue'),
TestIssueBody: () => import('../grouped_test_report/components/test_issue_body.vue'),
};
export const componentNames = {
- AccessibilityIssueBody: 'AccessibilityIssueBody',
CodequalityIssueBody: 'CodequalityIssueBody',
TestIssueBody: 'TestIssueBody',
};
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index 6061be465ca..bb86695b9a3 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -1,7 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
import api from '~/api';
-import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -115,9 +114,6 @@ export default {
},
computed: {
- collapseText() {
- return this.isCollapsed ? __('Expand') : __('Collapse');
- },
isLoading() {
return this.status === status.LOADING;
},
@@ -172,6 +168,11 @@ export default {
},
methods: {
toggleCollapsed() {
+ // Because the top-level div is always clickable, we need to check if we can collapse.
+ if (!this.isCollapsible) {
+ return;
+ }
+
if (this.trackAction) {
api.trackRedisHllUserEvent(this.trackAction);
}
@@ -186,10 +187,13 @@ export default {
</script>
<template>
<section class="media-section">
- <div class="media">
+ <div class="media" :class="{ 'gl-cursor-pointer': isCollapsible }" @click="toggleCollapsed">
<status-icon :status="statusIconName" :size="24" class="align-self-center" />
- <div class="media-body d-flex flex-align-self-center align-items-center">
- <div data-testid="report-section-code-text" class="js-code-text code-text">
+ <div class="media-body gl-display-flex gl-align-items-flex-start gl-flex-direction-row!">
+ <div
+ data-testid="report-section-code-text"
+ class="js-code-text code-text gl-align-self-center gl-flex-grow-1"
+ >
<div class="gl-display-flex gl-align-items-center">
<p class="gl-line-height-normal gl-m-0">{{ headerText }}</p>
<slot :name="slotName"></slot>
@@ -204,14 +208,19 @@ export default {
<slot name="action-buttons" :is-collapsible="isCollapsible"></slot>
- <gl-button
+ <div
v-if="isCollapsible"
- data-testid="report-section-expand-button"
- data-qa-selector="expand_report_button"
- @click="toggleCollapsed"
+ class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3"
>
- {{ collapseText }}
- </gl-button>
+ <gl-button
+ data-testid="report-section-expand-button"
+ data-qa-selector="expand_report_button"
+ category="tertiary"
+ size="small"
+ :icon="isExpanded ? 'chevron-lg-up' : 'chevron-lg-down'"
+ @click.stop="toggleCollapsed"
+ />
+ </div>
</div>
</div>
diff --git a/app/assets/javascripts/repository/commits_service.js b/app/assets/javascripts/repository/commits_service.js
index 5fd9cfd4e53..f009c0310c5 100644
--- a/app/assets/javascripts/repository/commits_service.js
+++ b/app/assets/javascripts/repository/commits_service.js
@@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { normalizeData } from 'ee_else_ce/repository/utils/commit';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { COMMIT_BATCH_SIZE, I18N_COMMIT_DATA_FETCH_ERROR } from './constants';
let requestedOffsets = [];
@@ -43,7 +43,7 @@ const fetchData = (projectPath, path, ref, offset) => {
return axios
.get(url, { params: { format: 'json', offset } })
.then(({ data }) => normalizeData(data, path))
- .catch(() => createFlash({ message: I18N_COMMIT_DATA_FETCH_ERROR }));
+ .catch(() => createAlert({ message: I18N_COMMIT_DATA_FETCH_ERROR }));
};
export const loadCommits = async (projectPath, path, ref, offset) => {
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 902077ba3e4..bf1667d8734 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -4,7 +4,7 @@ import { uniqueId } from 'lodash';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -271,7 +271,7 @@ export default {
.catch(() => this.displayError());
},
displayError() {
- createFlash({ message: __('An error occurred while loading the file. Please try again.') });
+ createAlert({ message: __('An error occurred while loading the file. Please try again.') });
},
switchViewer(newViewer) {
this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
diff --git a/app/assets/javascripts/repository/components/blob_controls.vue b/app/assets/javascripts/repository/components/blob_controls.vue
index fb1227f0df9..29c2c3762fc 100644
--- a/app/assets/javascripts/repository/components/blob_controls.vue
+++ b/app/assets/javascripts/repository/components/blob_controls.vue
@@ -1,9 +1,11 @@
<script>
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import getRefMixin from '~/repository/mixins/get_ref';
import initSourcegraph from '~/sourcegraph';
+import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
+import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
import { updateElementsVisibility } from '../utils/dom';
import blobControlsQuery from '../queries/blob_controls.query.graphql';
@@ -34,7 +36,7 @@ export default {
return !this.filePath;
},
error() {
- createFlash({ message: this.$options.i18n.errorMessage });
+ createAlert({ message: this.$options.i18n.errorMessage });
},
},
},
@@ -84,6 +86,33 @@ export default {
},
blobInfo() {
initSourcegraph();
+ this.$nextTick(() => {
+ this.initShortcuts();
+ this.initLinksUpdate();
+ });
+ },
+ },
+ methods: {
+ initShortcuts() {
+ const fileBlobPermalinkUrlElement = document.querySelector(
+ '.js-data-file-blob-permalink-url',
+ );
+ const fileBlobPermalinkUrl =
+ fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ // eslint-disable-next-line no-new
+ new ShortcutsBlob({
+ skipResetBindings: true,
+ fileBlobPermalinkUrl,
+ fileBlobPermalinkUrlElement,
+ });
+ },
+ initLinksUpdate() {
+ // eslint-disable-next-line no-new
+ new BlobLinePermalinkUpdater(
+ document.querySelector('.tree-holder'),
+ '.file-line-num[data-line-number], .file-line-num[data-line-number] *',
+ document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
+ );
},
},
};
@@ -99,6 +128,7 @@ export default {
data-testid="blame"
:href="blobInfo.blamePath"
:class="$options.buttonClassList"
+ class="js-blob-blame-link"
>
{{ $options.i18n.blame }}
</gl-button>
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 22fe3fe440e..05d64077866 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -122,14 +122,12 @@ export default {
:link-href="commit.author.webPath"
:img-src="commit.author.avatarUrl"
:img-size="32"
- :img-css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (7731 epics) */"
class="gl-my-2 gl-mr-4"
/>
<user-avatar-image
v-else
class="gl-my-2 gl-mr-4"
:img-src="commit.authorGravatar || $options.defaultAvatarUrl"
- :css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (7731 epics) */"
:size="32"
/>
<div
@@ -171,7 +169,7 @@ export default {
v-if="commitDescription"
v-safe-html:[$options.safeHtmlConfig]="commitDescription"
:class="{ 'd-block': showDescription }"
- class="commit-row-description gl-mb-3"
+ class="commit-row-description gl-mb-3 gl-white-space-pre-line"
></pre>
</div>
<div class="gl-flex-grow-1"></div>
diff --git a/app/assets/javascripts/repository/components/new_directory_modal.vue b/app/assets/javascripts/repository/components/new_directory_modal.vue
index 6c5797bf5b2..b28ebe7bb1e 100644
--- a/app/assets/javascripts/repository/components/new_directory_modal.vue
+++ b/app/assets/javascripts/repository/components/new_directory_modal.vue
@@ -8,7 +8,7 @@ import {
GlFormTextarea,
GlToggle,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -140,7 +140,7 @@ export default {
})
.catch(() => {
this.loading = false;
- createFlash({ message: ERROR_MESSAGE });
+ createAlert({ message: ERROR_MESSAGE });
});
},
},
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index c8cd64b5311..f3c5ace75fc 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -260,19 +260,19 @@ export default {
class="ml-1"
/>
</td>
- <td class="d-none d-sm-table-cell tree-commit cursor-default">
+ <td class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary">
<gl-link
v-if="commitData"
v-safe-html:[$options.safeHtmlConfig]="commitData.titleHtml"
:href="commitData.commitPath"
:title="commitData.message"
- class="str-truncated-100 tree-commit-link"
+ class="str-truncated-100 tree-commit-link gl-text-secondary"
/>
<gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
<gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</gl-intersection-observer>
</td>
- <td class="tree-time-ago text-right cursor-default">
+ <td class="tree-time-ago text-right cursor-default gl-text-secondary">
<timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
<gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</td>
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 2200e999c75..8a45a351c35 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -1,6 +1,6 @@
<script>
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '~/locale';
import {
@@ -142,7 +142,7 @@ export default {
}
})
.catch((error) => {
- createFlash({
+ createAlert({
message: __('An error occurred while fetching folder content.'),
});
throw error;
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
index 7fcaf772aac..4603ea2710d 100644
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue
@@ -9,7 +9,7 @@ import {
GlButton,
GlAlert,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
import { numberToHumanSize } from '~/lib/utils/number_utils';
@@ -171,7 +171,7 @@ export default {
})
.catch(() => {
this.loading = false;
- createFlash({ message: ERROR_MESSAGE });
+ createAlert({ message: ERROR_MESSAGE });
});
},
formData() {
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index f5620876783..dbaabb35cde 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -17,8 +17,6 @@ import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_cou
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
-import RunnerBulkDelete from '../components/runner_bulk_delete.vue';
-import RunnerBulkDeleteCheckbox from '../components/runner_bulk_delete_checkbox.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerListEmptyState from '../components/runner_list_empty_state.vue';
import RunnerName from '../components/runner_name.vue';
@@ -30,7 +28,12 @@ import RunnerActionsCell from '../components/cells/runner_actions_cell.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
-import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
+import {
+ ADMIN_FILTERED_SEARCH_NAMESPACE,
+ INSTANCE_TYPE,
+ I18N_FETCH_ERROR,
+ FILTER_CSS_CLASSES,
+} from '../constants';
import { captureException } from '../sentry_utils';
export default {
@@ -40,8 +43,6 @@ export default {
RegistrationDropdown,
RunnerStackedLayoutBanner,
RunnerFilteredSearchBar,
- RunnerBulkDelete,
- RunnerBulkDeleteCheckbox,
RunnerList,
RunnerListEmptyState,
RunnerName,
@@ -51,7 +52,7 @@ export default {
RunnerActionsCell,
},
mixins: [glFeatureFlagMixin()],
- inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'],
+ inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
props: {
registrationToken: {
type: String,
@@ -114,11 +115,6 @@ export default {
upgradeStatusTokenConfig,
];
},
- isBulkDeleteEnabled() {
- // Feature flag: admin_runners_bulk_delete
- // Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981
- return this.glFeatures.adminRunnersBulkDelete;
- },
isSearchFiltered() {
return isSearchFiltered(this.search);
},
@@ -155,18 +151,13 @@ export default {
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
- onChecked({ runner, isChecked }) {
- this.localMutations.setRunnerChecked({
- runner,
- isChecked,
- });
- },
onPaginationInput(value) {
this.search.pagination = value;
},
},
filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE,
+ FILTER_CSS_CLASSES,
};
</script>
<template>
@@ -195,6 +186,7 @@ export default {
<runner-filtered-search-bar
v-model="search"
+ :class="$options.FILTER_CSS_CLASSES"
:tokens="searchTokens"
:namespace="$options.filteredSearchNamespace"
/>
@@ -209,20 +201,12 @@ export default {
:filtered-svg-path="emptyStateFilteredSvgPath"
/>
<template v-else>
- <runner-bulk-delete
- v-if="isBulkDeleteEnabled"
- :runners="runners.items"
- @deleted="onDeleted"
- />
<runner-list
:runners="runners.items"
:loading="runnersLoading"
- :checkable="isBulkDeleteEnabled"
- @checked="onChecked"
+ :checkable="true"
+ @deleted="onDeleted"
>
- <template v-if="isBulkDeleteEnabled" #head-checkbox>
- <runner-bulk-delete-checkbox :runners="runners.items" />
- </template>
<template #runner-name="{ runner }">
<gl-link :href="runner.adminUrl">
<runner-name :runner="runner" />
diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
index 7a4760f81ee..13f520c4edb 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
@@ -52,11 +52,6 @@ export default {
:compact="true"
@toggledPaused="onToggledPaused"
/>
- <runner-delete-button
- :disabled="!canDelete"
- :runner="runner"
- :compact="true"
- @deleted="onDeleted"
- />
+ <runner-delete-button v-if="canDelete" :runner="runner" :compact="true" @deleted="onDeleted" />
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue b/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue
new file mode 100644
index 00000000000..cb43760b2d6
--- /dev/null
+++ b/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue
@@ -0,0 +1,63 @@
+<script>
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, I18N_ADMIN } from '../../constants';
+
+export default {
+ components: {
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ cell() {
+ switch (this.runner?.runnerType) {
+ case INSTANCE_TYPE:
+ return {
+ text: I18N_ADMIN,
+ };
+ case GROUP_TYPE: {
+ const { name, fullName, webUrl } = this.runner?.groups?.nodes[0] || {};
+
+ return {
+ text: name,
+ href: webUrl,
+ tooltip: fullName !== name ? fullName : '',
+ };
+ }
+ case PROJECT_TYPE: {
+ const { name, nameWithNamespace, webUrl } = this.runner?.ownerProject || {};
+
+ return {
+ text: name,
+ href: webUrl,
+ tooltip: nameWithNamespace !== name ? nameWithNamespace : '',
+ };
+ }
+ default:
+ return {};
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-link
+ v-if="cell.href"
+ v-gl-tooltip="cell.tooltip"
+ :href="cell.href"
+ class="gl-text-body gl-text-decoration-underline"
+ >
+ {{ cell.text }}
+ </gl-link>
+ <span v-else>{{ cell.text }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue
index dde5a5a4a05..75afb7a00bc 100644
--- a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue
+++ b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue
@@ -1,5 +1,6 @@
<script>
import { GlFormCheckbox } from '@gitlab/ui';
+import { s__ } from '~/locale';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
export default {
@@ -25,14 +26,20 @@ export default {
},
},
computed: {
+ deletableRunners() {
+ return this.runners.filter((runner) => runner.userPermissions?.deleteRunner);
+ },
disabled() {
- return !this.runners.length;
+ return !this.deletableRunners.length;
},
checked() {
- return Boolean(this.runners.length) && this.runners.every(this.isChecked);
+ return Boolean(this.deletableRunners.length) && this.deletableRunners.every(this.isChecked);
},
indeterminate() {
- return !this.checked && this.runners.some(this.isChecked);
+ return !this.checked && this.deletableRunners.some(this.isChecked);
+ },
+ label() {
+ return this.checked ? s__('Runners|Unselect all') : s__('Runners|Select all');
},
},
methods: {
@@ -41,7 +48,7 @@ export default {
},
onChange($event) {
this.localMutations.setRunnersChecked({
- runners: this.runners,
+ runners: this.deletableRunners,
isChecked: $event,
});
},
@@ -51,6 +58,7 @@ export default {
<template>
<gl-form-checkbox
+ :aria-label="label"
:indeterminate="indeterminate"
:checked="checked"
:disabled="disabled"
diff --git a/app/assets/javascripts/runner/components/runner_delete_button.vue b/app/assets/javascripts/runner/components/runner_delete_button.vue
index 62382891df0..b4f022a7d14 100644
--- a/app/assets/javascripts/runner/components/runner_delete_button.vue
+++ b/app/assets/javascripts/runner/components/runner_delete_button.vue
@@ -5,12 +5,7 @@ import { createAlert } from '~/flash';
import { sprintf } from '~/locale';
import { captureException } from '~/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import {
- I18N_DELETE_DISABLED_MANY_PROJECTS,
- I18N_DELETE_DISABLED_UNKNOWN_REASON,
- I18N_DELETE_RUNNER,
- I18N_DELETED_TOAST,
-} from '../constants';
+import { I18N_DELETE_RUNNER, I18N_DELETED_TOAST } from '../constants';
import RunnerDeleteModal from './runner_delete_modal.vue';
export default {
@@ -31,11 +26,6 @@ export default {
return runner?.id && runner?.shortSha;
},
},
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
compact: {
type: Boolean,
required: false,
@@ -85,29 +75,14 @@ export default {
return null;
},
tooltip() {
- if (this.disabled && this.runner.projectCount > 1) {
- return I18N_DELETE_DISABLED_MANY_PROJECTS;
- }
- if (this.disabled) {
- return I18N_DELETE_DISABLED_UNKNOWN_REASON;
- }
-
// Only show basic "delete" tooltip when compact.
// Also prevent a "sticky" tooltip: If this button is
- // disabled, mouseout listeners don't run leaving the tooltip stuck
+ // loading, mouseout listeners don't run leaving the tooltip stuck
if (this.compact && !this.deleting) {
return I18N_DELETE_RUNNER;
}
return '';
},
- wrapperTabindex() {
- if (this.disabled) {
- // Trigger tooltip on keyboard-focusable wrapper
- // See https://bootstrap-vue.org/docs/directives/tooltip
- return '0';
- }
- return null;
- },
},
methods: {
async onDelete() {
@@ -156,14 +131,13 @@ export default {
</script>
<template>
- <div v-gl-tooltip="tooltip" class="btn-group" :tabindex="wrapperTabindex">
+ <div v-gl-tooltip="tooltip" class="btn-group">
<gl-button
v-gl-modal="runnerDeleteModalId"
:aria-label="ariaLabel"
:icon="icon"
:class="buttonClass"
:loading="deleting"
- :disabled="disabled"
variant="danger"
category="secondary"
v-bind="$attrs"
diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue
index 79f934764c6..3d72abcd393 100644
--- a/app/assets/javascripts/runner/components/runner_details.vue
+++ b/app/assets/javascripts/runner/components/runner_details.vue
@@ -4,7 +4,6 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
import RunnerDetail from './runner_detail.vue';
@@ -29,7 +28,6 @@ export default {
RunnerTags,
TimeAgo,
},
- mixins: [glFeatureFlagMixin()],
props: {
runner: {
type: Object,
@@ -117,10 +115,7 @@ export default {
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
- <runner-detail
- v-if="glFeatures.enforceRunnerTokenExpiresAt"
- :empty-value="s__('Runners|Never expires')"
- >
+ <runner-detail :empty-value="s__('Runners|Never expires')">
<template #label>
{{ s__('Runners|Token expiry') }}
<help-popover :options="tokenExpirationHelpPopoverOptions">
diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
index 5a9ab21a457..da59de9a9eb 100644
--- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
@@ -85,7 +85,6 @@ export default {
</script>
<template>
<filtered-search
- class="gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1"
v-bind="$attrs"
:namespace="namespace"
recent-searches-storage-key="runners-search"
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue
index 26f1f3ce08c..e895537dcdc 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/runner/components/runner_list.vue
@@ -2,15 +2,20 @@
import { GlFormCheckbox, GlTableLite, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
import { formatJobCount, tableField } from '../utils';
+import RunnerBulkDelete from './runner_bulk_delete.vue';
+import RunnerBulkDeleteCheckbox from './runner_bulk_delete_checkbox.vue';
import RunnerStackedSummaryCell from './cells/runner_stacked_summary_cell.vue';
import RunnerStatusPopover from './runner_status_popover.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue';
+import RunnerOwnerCell from './cells/runner_owner_cell.vue';
const defaultFields = [
tableField({ key: 'status', label: s__('Runners|Status'), thClasses: ['gl-w-15p'] }),
tableField({ key: 'summary', label: s__('Runners|Runner') }),
+ tableField({ key: 'owner', label: s__('Runners|Owner'), thClasses: ['gl-w-20p'] }),
tableField({ key: 'actions', label: '', thClasses: ['gl-w-15p'] }),
];
@@ -19,9 +24,13 @@ export default {
GlFormCheckbox,
GlTableLite,
GlSkeletonLoader,
+ HelpPopover,
+ RunnerBulkDelete,
+ RunnerBulkDeleteCheckbox,
RunnerStatusPopover,
RunnerStackedSummaryCell,
RunnerStatusCell,
+ RunnerOwnerCell,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -34,6 +43,7 @@ export default {
},
},
},
+ inject: ['localMutations'],
props: {
checkable: {
type: Boolean,
@@ -50,7 +60,7 @@ export default {
required: true,
},
},
- emits: ['checked'],
+ emits: ['deleted'],
data() {
return { checkedRunnerIds: [] };
},
@@ -79,6 +89,12 @@ export default {
},
},
methods: {
+ canDelete(runner) {
+ return runner.userPermissions?.deleteRunner;
+ },
+ onDeleted(event) {
+ this.$emit('deleted', event);
+ },
formatJobCount(jobCount) {
return formatJobCount(jobCount);
},
@@ -91,7 +107,7 @@ export default {
return {};
},
onCheckboxChange(runner, isChecked) {
- this.$emit('checked', {
+ this.localMutations.setRunnerChecked({
runner,
isChecked,
});
@@ -104,6 +120,7 @@ export default {
</script>
<template>
<div>
+ <runner-bulk-delete v-if="checkable" :runners="runners" @deleted="onDeleted" />
<gl-table-lite
:aria-busy="loading"
:class="tableClass"
@@ -116,11 +133,15 @@ export default {
fixed
>
<template #head(checkbox)>
- <slot name="head-checkbox"></slot>
+ <runner-bulk-delete-checkbox :runners="runners" />
</template>
<template #cell(checkbox)="{ item }">
- <gl-form-checkbox :checked="isChecked(item)" @change="onCheckboxChange(item, $event)" />
+ <gl-form-checkbox
+ v-if="canDelete(item)"
+ :checked="isChecked(item)"
+ @change="onCheckboxChange(item, $event)"
+ />
</template>
<template #head(status)="{ label }">
@@ -140,6 +161,21 @@ export default {
</runner-stacked-summary-cell>
</template>
+ <template #head(owner)="{ label }">
+ {{ label }}
+ <help-popover>
+ {{
+ s__(
+ 'Runners|The project, group or instance where the runner was registered. Instance runners are always owned by Administrator.',
+ )
+ }}
+ </help-popover>
+ </template>
+
+ <template #cell(owner)="{ item }">
+ <runner-owner-cell :runner="item" />
+ </template>
+
<template #cell(actions)="{ item }">
<slot name="runner-actions-cell" :runner="item"></slot>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/runner/components/runner_list_empty_state.vue
index ab9cde6a401..e6576c83e69 100644
--- a/app/assets/javascripts/runner/components/runner_list_empty_state.vue
+++ b/app/assets/javascripts/runner/components/runner_list_empty_state.vue
@@ -53,7 +53,7 @@ export default {
:svg-path="svgPath"
:svg-height="$options.svgHeight"
>
- <template #description>
+ <template v-if="registrationToken" #description>
<gl-sprintf
:message="
s__(
@@ -71,5 +71,12 @@ export default {
:registration-token="registrationToken"
/>
</template>
+ <template v-else #description>
+ {{
+ s__(
+ 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.',
+ )
+ }}
+ </template>
</gl-empty-state>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_membership_toggle.vue b/app/assets/javascripts/runner/components/runner_membership_toggle.vue
new file mode 100644
index 00000000000..2b37b1cc797
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_membership_toggle.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlToggle } from '@gitlab/ui';
+import {
+ I18N_SHOW_ONLY_INHERITED,
+ MEMBERSHIP_DESCENDANTS,
+ MEMBERSHIP_ALL_AVAILABLE,
+} from '../constants';
+
+export default {
+ components: {
+ GlToggle,
+ },
+ props: {
+ value: {
+ type: String,
+ default: MEMBERSHIP_DESCENDANTS,
+ required: false,
+ },
+ },
+ computed: {
+ toggle() {
+ return this.value === MEMBERSHIP_DESCENDANTS;
+ },
+ },
+ methods: {
+ onChange(value) {
+ this.$emit('input', value ? MEMBERSHIP_DESCENDANTS : MEMBERSHIP_ALL_AVAILABLE);
+ },
+ },
+ I18N_SHOW_ONLY_INHERITED,
+};
+</script>
+
+<template>
+ <gl-toggle
+ data-testid="runner-membership-toggle"
+ :value="toggle"
+ :label="$options.I18N_SHOW_ONLY_INHERITED"
+ label-position="left"
+ @change="onChange"
+ />
+</template>
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
index 59230bb809e..6e7c41885f8 100644
--- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
@@ -7,6 +7,12 @@ import { s__ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { RUNNER_TAG_BG_CLASS } from '../../constants';
+// TODO This should be implemented via a GraphQL API
+// The API should
+// 1) scope to the rights of the user
+// 2) stay up to date to the removal of old tags
+// 3) consider the scope of search, like searching within the tags of a group
+// See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796
export const TAG_SUGGESTIONS_PATH = '/admin/runners/tag_list.json';
export default {
@@ -29,12 +35,6 @@ export default {
},
methods: {
getTagsOptions(search) {
- // TODO This should be implemented via a GraphQL API
- // The API should
- // 1) scope to the rights of the user
- // 2) stay up to date to the removal of old tags
- // 3) consider the scope of search, like searching within the tags of a group
- // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796
return axios
.get(TAG_SUGGESTIONS_PATH, {
params: {
@@ -46,6 +46,12 @@ export default {
});
},
async fetchTags(searchTerm) {
+ // Note: Suggestions should only be enabled for admin users
+ if (this.config.suggestionsDisabled) {
+ this.tags = [];
+ return;
+ }
+
this.loading = true;
try {
this.tags = await this.getTagsOptions(searchTerm);
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index 3009577599f..dfc5f0c4152 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -11,6 +11,9 @@ export const RUNNER_DETAILS_JOBS_PAGE_SIZE = 30;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
+export const FILTER_CSS_CLASSES =
+ 'gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1';
+
// Type
export const I18N_ALL_TYPES = s__('Runners|All');
@@ -76,12 +79,6 @@ export const I18N_RESUME = __('Resume');
export const I18N_RESUME_TOOLTIP = s__('Runners|Resume accepting jobs');
export const I18N_DELETE_RUNNER = s__('Runners|Delete runner');
-export const I18N_DELETE_DISABLED_MANY_PROJECTS = s__(
- 'Runners|Multi-project runners cannot be deleted',
-);
-export const I18N_DELETE_DISABLED_UNKNOWN_REASON = s__(
- 'Runners|Runner cannot be deleted, please contact your administrator',
-);
export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
// List
@@ -91,6 +88,8 @@ export const I18N_LOCKED_RUNNER_DESCRIPTION = s__(
export const I18N_VERSION_LABEL = s__('Runners|Version %{version}');
export const I18N_LAST_CONTACT_LABEL = s__('Runners|Last contact: %{timeAgo}');
export const I18N_CREATED_AT_LABEL = s__('Runners|Created %{timeAgo}');
+export const I18N_SHOW_ONLY_INHERITED = s__('Runners|Show only inherited');
+export const I18N_ADMIN = s__('Runners|Administrator');
// Runner details
@@ -116,6 +115,7 @@ export const PARAM_KEY_PAUSED = 'paused';
export const PARAM_KEY_RUNNER_TYPE = 'runner_type';
export const PARAM_KEY_TAG = 'tag';
export const PARAM_KEY_SEARCH = 'search';
+export const PARAM_KEY_MEMBERSHIP = 'membership';
export const PARAM_KEY_SORT = 'sort';
export const PARAM_KEY_AFTER = 'after';
@@ -148,6 +148,13 @@ export const CONTACTED_ASC = 'CONTACTED_ASC';
export const DEFAULT_SORT = CREATED_DESC;
+// CiRunnerMembershipFilter
+
+export const MEMBERSHIP_DESCENDANTS = 'DESCENDANTS';
+export const MEMBERSHIP_ALL_AVAILABLE = 'ALL_AVAILABLE';
+
+export const DEFAULT_MEMBERSHIP = MEMBERSHIP_DESCENDANTS;
+
// Local storage namespaces
export const ADMIN_FILTERED_SEARCH_NAMESPACE = 'admin_runners';
diff --git a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql
index 4c519b9b867..95f9dd1beb9 100644
--- a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql
@@ -2,6 +2,7 @@
query getGroupRunners(
$groupFullPath: ID!
+ $membership: CiRunnerMembershipFilter
$before: String
$after: String
$first: Int
@@ -9,13 +10,14 @@ query getGroupRunners(
$paused: Boolean
$status: CiRunnerStatus
$type: CiRunnerType
+ $tagList: [String!]
$search: String
$sort: CiRunnerSort
) {
group(fullPath: $groupFullPath) {
id # Apollo required
runners(
- membership: DESCENDANTS
+ membership: $membership
before: $before
after: $after
first: $first
@@ -23,6 +25,7 @@ query getGroupRunners(
paused: $paused
status: $status
type: $type
+ tagList: $tagList
search: $search
sort: $sort
) {
diff --git a/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql b/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql
index 958b4ea0dd3..e88a2c2e7e6 100644
--- a/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql
+++ b/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql
@@ -1,5 +1,6 @@
query getGroupRunnersCount(
$groupFullPath: ID!
+ $membership: CiRunnerMembershipFilter
$paused: Boolean
$status: CiRunnerStatus
$type: CiRunnerType
@@ -9,7 +10,7 @@ query getGroupRunnersCount(
group(fullPath: $groupFullPath) {
id # Apollo required
runners(
- membership: DESCENDANTS
+ membership: $membership
paused: $paused
status: $status
type: $type
diff --git a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql
index a12ba7a751a..0dff011daaa 100644
--- a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql
@@ -16,4 +16,18 @@ fragment ListItemShared on CiRunner {
updateRunner
deleteRunner
}
+ groups(first: 1) {
+ nodes {
+ id
+ name
+ fullName
+ webUrl
+ }
+ }
+ ownerProject {
+ id
+ name
+ nameWithNamespace
+ webUrl
+ }
}
diff --git a/app/assets/javascripts/runner/graphql/list/local_state.js b/app/assets/javascripts/runner/graphql/list/local_state.js
index 154af261bba..e0477c660b4 100644
--- a/app/assets/javascripts/runner/graphql/list/local_state.js
+++ b/app/assets/javascripts/runner/graphql/list/local_state.js
@@ -20,10 +20,6 @@ import typeDefs from './typedefs.graphql';
* localMutations.setRunnerChecked( ... )
* ```
*
- * Note: Currently only in use behind a feature flag:
- * admin_runners_bulk_delete for the admin list, rollout issue:
- * https://gitlab.com/gitlab-org/gitlab/-/issues/353981
- *
* @returns {Object} An object to configure an Apollo client:
* contains cacheConfig, typeDefs, localMutations.
*/
@@ -52,16 +48,18 @@ export const createLocalState = () => {
const localMutations = {
setRunnerChecked({ runner, isChecked }) {
- checkedRunnerIdsVar({
- ...checkedRunnerIdsVar(),
- [runner.id]: isChecked,
- });
+ const { id, userPermissions } = runner;
+ if (userPermissions?.deleteRunner) {
+ checkedRunnerIdsVar({
+ ...checkedRunnerIdsVar(),
+ [id]: isChecked,
+ });
+ }
},
setRunnersChecked({ runners, isChecked }) {
- const newVal = runners.reduce(
- (acc, { id }) => ({ ...acc, [id]: isChecked }),
- checkedRunnerIdsVar(),
- );
+ const newVal = runners
+ .filter(({ userPermissions }) => userPermissions?.deleteRunner)
+ .reduce((acc, { id }) => ({ ...acc, [id]: isChecked }), checkedRunnerIdsVar());
checkedRunnerIdsVar(newVal);
},
clearChecked() {
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index 70826a6bfa1..7f56d895682 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -10,7 +10,9 @@ import {
fromSearchToVariables,
isSearchFiltered,
} from 'ee_else_ce/runner/runner_search_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue';
@@ -22,14 +24,17 @@ import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import RunnerActionsCell from '../components/cells/runner_actions_cell.vue';
+import RunnerMembershipToggle from '../components/runner_membership_toggle.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
+import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
import {
GROUP_FILTERED_SEARCH_NAMESPACE,
GROUP_TYPE,
PROJECT_TYPE,
I18N_FETCH_ERROR,
+ FILTER_CSS_CLASSES,
} from '../constants';
import { captureException } from '../sentry_utils';
@@ -43,11 +48,13 @@ export default {
RunnerList,
RunnerListEmptyState,
RunnerName,
+ RunnerMembershipToggle,
RunnerStats,
RunnerPagination,
RunnerTypeTabs,
RunnerActionsCell,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
props: {
registrationToken: {
@@ -126,12 +133,20 @@ export default {
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
- searchTokens() {
- return [pausedTokenConfig, statusTokenConfig, upgradeStatusTokenConfig];
- },
filteredSearchNamespace() {
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
},
+ searchTokens() {
+ return [
+ pausedTokenConfig,
+ statusTokenConfig,
+ {
+ ...tagTokenConfig,
+ suggestionsDisabled: true,
+ },
+ upgradeStatusTokenConfig,
+ ];
+ },
isSearchFiltered() {
return isSearchFiltered(this.search);
},
@@ -159,13 +174,17 @@ export default {
editUrl(runner) {
return this.runners.urlsById[runner.id]?.edit;
},
+ refetchCounts() {
+ this.$apollo.getClient().refetchQueries({ include: [groupRunnersCountQuery] });
+ },
onToggledPaused() {
// When a runner becomes Paused, the tab count can
// become stale, refetch outdated counts.
- this.$refs['runner-type-tabs'].refetch();
+ this.refetchCounts();
},
onDeleted({ message }) {
this.$root.$toast?.show(message);
+ this.refetchCounts();
},
reportToSentry(error) {
captureException({ error, component: this.$options.name });
@@ -176,6 +195,7 @@ export default {
},
TABS_RUNNER_TYPES: [GROUP_TYPE, PROJECT_TYPE],
GROUP_TYPE,
+ FILTER_CSS_CLASSES,
};
</script>
@@ -204,11 +224,21 @@ export default {
/>
</div>
- <runner-filtered-search-bar
- v-model="search"
- :tokens="searchTokens"
- :namespace="filteredSearchNamespace"
- />
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-gap-3"
+ :class="$options.FILTER_CSS_CLASSES"
+ >
+ <runner-filtered-search-bar
+ v-model="search"
+ :tokens="searchTokens"
+ :namespace="filteredSearchNamespace"
+ class="gl-flex-grow-1 gl-align-self-stretch"
+ />
+ <runner-membership-toggle
+ v-model="search.membership"
+ class="gl-align-self-end gl-md-align-self-center"
+ />
+ </div>
<runner-stats :scope="$options.GROUP_TYPE" :variables="countVariables" />
@@ -220,7 +250,7 @@ export default {
:filtered-svg-path="emptyStateFilteredSvgPath"
/>
<template v-else>
- <runner-list :runners="runners.items" :loading="runnersLoading">
+ <runner-list :runners="runners.items" :loading="runnersLoading" @deleted="onDeleted">
<template #runner-name="{ runner }">
<gl-link :href="webUrl(runner)">
<runner-name :runner="runner" />
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js
index feed6b0ceb7..0e7efd2b8a1 100644
--- a/app/assets/javascripts/runner/group_runners/index.js
+++ b/app/assets/javascripts/runner/group_runners/index.js
@@ -2,6 +2,7 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { createLocalState } from '../graphql/list/local_state';
import GroupRunnersApp from './group_runners_app.vue';
Vue.use(GlToast);
@@ -26,8 +27,10 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
emptyStateFilteredSvgPath,
} = el.dataset;
+ const { cacheConfig, typeDefs, localMutations } = createLocalState();
+
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { cacheConfig, typeDefs }),
});
return new Vue({
@@ -35,6 +38,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
apolloProvider,
provide: {
runnerInstallHelpPage,
+ localMutations,
groupId,
onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10),
staleTimeoutSecs: parseInt(staleTimeoutSecs, 10),
diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js
index dc582ccbac1..adc832b0600 100644
--- a/app/assets/javascripts/runner/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_search_utils.js
@@ -13,10 +13,12 @@ import {
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
PARAM_KEY_SEARCH,
+ PARAM_KEY_MEMBERSHIP,
PARAM_KEY_SORT,
PARAM_KEY_AFTER,
PARAM_KEY_BEFORE,
DEFAULT_SORT,
+ DEFAULT_MEMBERSHIP,
RUNNER_PAGE_SIZE,
} from './constants';
import { getPaginationVariables } from './utils';
@@ -57,9 +59,10 @@ import { getPaginationVariables } from './utils';
* @param {Object} search
* @returns {boolean} True if the value follows the search format.
*/
-export const searchValidator = ({ runnerType, filters, sort }) => {
+export const searchValidator = ({ runnerType, membership, filters, sort }) => {
return (
(runnerType === null || typeof runnerType === 'string') &&
+ (membership === null || typeof membership === 'string') &&
Array.isArray(filters) &&
typeof sort === 'string'
);
@@ -140,9 +143,11 @@ export const updateOutdatedUrl = (url = window.location.href) => {
export const fromUrlQueryToSearch = (query = window.location.search) => {
const params = queryToObject(query, { gatherArrays: true });
const runnerType = params[PARAM_KEY_RUNNER_TYPE]?.[0] || null;
+ const membership = params[PARAM_KEY_MEMBERSHIP]?.[0] || null;
return {
runnerType,
+ membership: membership || DEFAULT_MEMBERSHIP,
filters: prepareTokens(
urlQueryToFilter(query, {
filterNamesAllowList: [PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG],
@@ -162,13 +167,14 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
* @returns {String} New URL for the page
*/
export const fromSearchToUrl = (
- { runnerType = null, filters = [], sort = null, pagination = {} },
+ { runnerType = null, membership = null, filters = [], sort = null, pagination = {} },
url = window.location.href,
) => {
const filterParams = {
// Defaults
[PARAM_KEY_STATUS]: [],
[PARAM_KEY_RUNNER_TYPE]: [],
+ [PARAM_KEY_MEMBERSHIP]: [],
[PARAM_KEY_TAG]: [],
// Current filters
...filterToQueryObject(processFilters(filters), {
@@ -180,6 +186,10 @@ export const fromSearchToUrl = (
filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType];
}
+ if (membership && membership !== DEFAULT_MEMBERSHIP) {
+ filterParams[PARAM_KEY_MEMBERSHIP] = [membership];
+ }
+
if (!filterParams[PARAM_KEY_SEARCH]) {
filterParams[PARAM_KEY_SEARCH] = null;
}
@@ -203,6 +213,7 @@ export const fromSearchToUrl = (
*/
export const fromSearchToVariables = ({
runnerType = null,
+ membership = null,
filters = [],
sort = null,
pagination = {},
@@ -226,6 +237,9 @@ export const fromSearchToVariables = ({
if (runnerType) {
filterVariables.type = runnerType;
}
+ if (membership) {
+ filterVariables.membership = membership;
+ }
if (sort) {
filterVariables.sort = sort;
}
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index 446ab7f433c..ba12f31ef87 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -1,7 +1,7 @@
import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import { queryToObject } from '~/lib/utils/url_utility';
import refreshCounts from '~/pages/search/show/refresh_counts';
-import { initSidebar } from './sidebar';
+import { initSidebar, sidebarInitState } from './sidebar';
import { initSearchSort } from './sort';
import createStore from './store';
import { initTopbar } from './topbar';
@@ -9,14 +9,18 @@ import { initBlobRefSwitcher } from './under_topbar';
export const initSearchApp = () => {
const query = queryToObject(window.location.search);
+ const navigation = sidebarInitState();
- const store = createStore({ query });
+ const store = createStore({ query, navigation });
initTopbar(store);
initSidebar(store);
initSearchSort(store);
setHighlightClass(query.search); // Code Highlighting
- refreshCounts(); // Other Scope Tab Counts
initBlobRefSwitcher(); // Code Search Branch Picker
+
+ if (!gon.features?.searchPageVerticalNav) {
+ refreshCounts(); // Other Scope Tab Counts
+ }
};
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 5c7cbeac5b2..789efc8f09d 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -17,6 +17,9 @@ export default {
showReset() {
return this.urlQuery.state || this.urlQuery.confidential;
},
+ showSidebar() {
+ return this.urlQuery.scope === 'issues' || this.urlQuery.scope === 'merge_requests';
+ },
},
methods: {
...mapActions(['applyQuery', 'resetQuery']),
@@ -29,15 +32,17 @@ export default {
class="search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4 gl-mb-6 gl-mt-5"
@submit.prevent="applyQuery"
>
- <status-filter />
- <confidentiality-filter />
- <div class="gl-display-flex gl-align-items-center gl-mt-3">
- <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
- {{ __('Apply') }}
- </gl-button>
- <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
- __('Reset filters')
- }}</gl-link>
- </div>
+ <template v-if="showSidebar">
+ <status-filter />
+ <confidentiality-filter />
+ <div class="gl-display-flex gl-align-items-center gl-mt-3">
+ <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
+ {{ __('Apply') }}
+ </gl-button>
+ <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
+ __('Reset filters')
+ }}</gl-link>
+ </div>
+ </template>
</form>
</template>
diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js
index 1414adcac27..c6b1257c4ef 100644
--- a/app/assets/javascripts/search/sidebar/index.js
+++ b/app/assets/javascripts/search/sidebar/index.js
@@ -4,6 +4,15 @@ import GlobalSearchSidebar from './components/app.vue';
Vue.use(Translate);
+export const sidebarInitState = () => {
+ const el = document.getElementById('js-search-sidebar');
+
+ if (!el) return {};
+
+ const { navigation } = el.dataset;
+ return JSON.parse(navigation);
+};
+
export const initSidebar = (store) => {
const el = document.getElementById('js-search-sidebar');
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index dc8b6201953..be5742e5949 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, SIDEBAR_PARAMS } from './constants';
@@ -13,7 +13,7 @@ export const fetchGroups = ({ commit }, search) => {
commit(types.RECEIVE_GROUPS_SUCCESS, data);
})
.catch(() => {
- createFlash({ message: __('There was a problem fetching groups.') });
+ createAlert({ message: __('There was a problem fetching groups.') });
commit(types.RECEIVE_GROUPS_ERROR);
});
};
@@ -23,7 +23,7 @@ export const fetchProjects = ({ commit, state }, search) => {
const groupId = state.query?.group_id;
const handleCatch = () => {
- createFlash({ message: __('There was an error fetching projects') });
+ createAlert({ message: __('There was an error fetching projects') });
commit(types.RECEIVE_PROJECTS_ERROR);
};
const handleSuccess = ({ data }) => {
@@ -59,7 +59,7 @@ export const loadFrequentGroups = async ({ commit, state }) => {
const inflatedData = mergeById(await Promise.all(promises), storedData);
commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: inflatedData });
} catch {
- createFlash({ message: __('There was a problem fetching recent groups.') });
+ createAlert({ message: __('There was a problem fetching recent groups.') });
}
};
@@ -70,7 +70,7 @@ export const loadFrequentProjects = async ({ commit, state }) => {
const inflatedData = mergeById(await Promise.all(promises), storedData);
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: inflatedData });
} catch {
- createFlash({ message: __('There was a problem fetching recent projects.') });
+ createAlert({ message: __('There was a problem fetching recent projects.') });
}
};
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
index f27dae8249d..d0fcbb0d83b 100644
--- a/app/assets/javascripts/search/topbar/components/app.vue
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -1,7 +1,9 @@
<script>
import { GlSearchBoxByClick } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
+import { parseBoolean } from '~/lib/utils/common_utils';
import GroupFilter from './group_filter.vue';
import ProjectFilter from './project_filter.vue';
@@ -16,6 +18,7 @@ export default {
GroupFilter,
ProjectFilter,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
groupInitialData: {
type: Object,
@@ -39,7 +42,10 @@ export default {
},
},
showFilters() {
- return !this.query.snippets || this.query.snippets === 'false';
+ return !parseBoolean(this.query.snippets);
+ },
+ hasVerticalNav() {
+ return this.glFeatures.searchPageVerticalNav;
},
},
created() {
@@ -52,24 +58,27 @@ export default {
</script>
<template>
- <section class="search-page-form gl-lg-display-flex gl-align-items-flex-end">
- <div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
- <label>{{ $options.i18n.searchLabel }}</label>
- <gl-search-box-by-click
- id="dashboard_search"
- v-model="search"
- name="search"
- :placeholder="$options.i18n.searchPlaceholder"
- @submit="applyQuery"
- />
- </div>
- <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
- <label class="gl-display-block">{{ __('Group') }}</label>
- <group-filter :initial-data="groupInitialData" />
- </div>
- <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
- <label class="gl-display-block">{{ __('Project') }}</label>
- <project-filter :initial-data="projectInitialData" />
+ <section class="search-page-form gl-lg-display-flex gl-flex-direction-column">
+ <div class="gl-lg-display-flex gl-flex-direction-row gl-align-items-flex-end">
+ <div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
+ <label>{{ $options.i18n.searchLabel }}</label>
+ <gl-search-box-by-click
+ id="dashboard_search"
+ v-model="search"
+ name="search"
+ :placeholder="$options.i18n.searchPlaceholder"
+ @submit="applyQuery"
+ />
+ </div>
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
+ <label class="gl-display-block">{{ __('Group') }}</label>
+ <group-filter :initial-data="groupInitialData" />
+ </div>
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
+ <label class="gl-display-block">{{ __('Project') }}</label>
+ <project-filter :initial-data="projectInitialData" />
+ </div>
</div>
+ <hr v-if="hasVerticalNav" class="gl-mt-5 gl-mb-0 gl-border-gray-100" />
</section>
</template>
diff --git a/app/assets/javascripts/search_settings/components/search_settings.vue b/app/assets/javascripts/search_settings/components/search_settings.vue
index e9cc9616fd0..3fc279f363a 100644
--- a/app/assets/javascripts/search_settings/components/search_settings.vue
+++ b/app/assets/javascripts/search_settings/components/search_settings.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSearchBoxByType } from '@gitlab/ui';
+import { GlEmptyState, GlSearchBoxByType } from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
import {
EXCLUDED_NODES,
@@ -96,6 +96,8 @@ const displayResults = ({ sectionSelector, expandSection, searchTerm }, matching
hideSectionsExcept(sectionSelector, sections);
sections.forEach(expandSection);
highlightText(matchingTextNodes, searchTerm);
+
+ return sections.length > 0;
};
const clearResults = (params) => {
@@ -126,6 +128,7 @@ const search = (root, searchTerm) => {
export default {
components: {
+ GlEmptyState,
GlSearchBoxByType,
},
props: {
@@ -137,6 +140,11 @@ export default {
type: String,
required: true,
},
+ hideWhenEmptySelector: {
+ type: String,
+ required: true,
+ default: null,
+ },
isExpandedFn: {
type: Function,
required: false,
@@ -147,8 +155,16 @@ export default {
data() {
return {
searchTerm: '',
+ hasMatches: true,
};
},
+ watch: {
+ hasMatches(newHasMatches) {
+ document.querySelectorAll(this.hideWhenEmptySelector).forEach((section) => {
+ section.classList.toggle(HIDE_CLASS, !newHasMatches);
+ });
+ },
+ },
methods: {
search(value) {
this.searchTerm = value;
@@ -161,11 +177,12 @@ export default {
};
clearResults(displayOptions);
+ this.hasMatches = true;
if (value.length) {
saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions);
- displayResults(displayOptions, search(this.searchRoot, this.searchTerm));
+ this.hasMatches = displayResults(displayOptions, search(this.searchRoot, this.searchTerm));
} else {
restoreExpansionState(displayOptions);
}
@@ -181,10 +198,18 @@ export default {
};
</script>
<template>
- <gl-search-box-by-type
- :value="searchTerm"
- :debounce="$options.TYPING_DELAY"
- :placeholder="__('Search page')"
- @input="search"
- />
+ <div>
+ <gl-search-box-by-type
+ :value="searchTerm"
+ :debounce="$options.TYPING_DELAY"
+ :placeholder="__('Search page')"
+ @input="search"
+ />
+
+ <gl-empty-state
+ v-if="!hasMatches"
+ :title="__('No results found')"
+ :description="__('Edit your search and try again')"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/search_settings/mount.js b/app/assets/javascripts/search_settings/mount.js
index b1086f9ca1f..b727b55781a 100644
--- a/app/assets/javascripts/search_settings/mount.js
+++ b/app/assets/javascripts/search_settings/mount.js
@@ -11,6 +11,7 @@ const mountSearch = ({ el }) =>
props: {
searchRoot: document.querySelector('#content-body'),
sectionSelector: '.js-search-settings-section, section.settings',
+ hideWhenEmptySelector: '.js-hide-when-nothing-matches-search',
isExpandedFn: isExpanded,
},
on: {
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index ecde9235e93..7828efc358a 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -21,7 +21,9 @@ export const i18n = {
),
description: s__(
`SecurityConfiguration|Once you've enabled a scan for the default branch,
- any subsequent feature branch you create will include the scan.`,
+ any subsequent feature branch you create will include the scan. An enabled
+ scanner will not be reflected as such until the pipeline has been
+ successfully executed and it has generated valid artifacts.`,
),
securityConfiguration: __('Security Configuration'),
vulnerabilityManagement: s__('SecurityConfiguration|Vulnerability Management'),
@@ -165,7 +167,12 @@ export default {
</template>
</user-callout-dismisser>
- <gl-tabs content-class="gl-pt-0" sync-active-tab-with-query-params lazy>
+ <gl-tabs
+ content-class="gl-pt-0"
+ data-qa-selector="security_configuration_container"
+ sync-active-tab-with-query-params
+ lazy
+ >
<gl-tab
data-testid="security-testing-tab"
:title="$options.i18n.securityTesting"
diff --git a/app/assets/javascripts/security_configuration/components/upgrade_banner.vue b/app/assets/javascripts/security_configuration/components/upgrade_banner.vue
index 891d7bf2eb0..eaff1ce6055 100644
--- a/app/assets/javascripts/security_configuration/components/upgrade_banner.vue
+++ b/app/assets/javascripts/security_configuration/components/upgrade_banner.vue
@@ -22,7 +22,7 @@ export default {
s__('SecurityConfiguration|High-level vulnerability statistics across projects and groups'),
s__('SecurityConfiguration|Runtime security metrics for application environments'),
s__(
- 'SecurityConfiguration|More scan types, including Container Scanning, DAST, Dependency Scanning, Fuzzing, and Licence Compliance',
+ 'SecurityConfiguration|More scan types, including DAST, Dependency Scanning, Fuzzing, and Licence Compliance',
),
],
buttonText: s__('SecurityConfiguration|Upgrade or start a free trial'),
diff --git a/app/assets/javascripts/service_ping_consent.js b/app/assets/javascripts/service_ping_consent.js
index f145a1b30db..f2c3f28cefa 100644
--- a/app/assets/javascripts/service_ping_consent.js
+++ b/app/assets/javascripts/service_ping_consent.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import createFlash, { hideFlash } from './flash';
+import { createAlert, hideFlash } from './flash';
import axios from './lib/utils/axios_utils';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
@@ -27,7 +27,7 @@ export default () => {
})
.catch(() => {
hideConsentMessage();
- createFlash({
+ createAlert({
message: __('Something went wrong. Try again later.'),
});
});
diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue
index 7f9a30b7ff1..86049a2b781 100644
--- a/app/assets/javascripts/set_status_modal/set_status_form.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_form.vue
@@ -131,9 +131,9 @@ export default {
i18n: {
statusMessagePlaceholder: s__(`SetStatusModal|What's your status?`),
clearStatusButtonLabel: s__('SetStatusModal|Clear status'),
- availabilityCheckboxLabel: s__('SetStatusModal|Busy'),
+ availabilityCheckboxLabel: s__('SetStatusModal|Set yourself as busy'),
availabilityCheckboxHelpText: s__(
- 'SetStatusModal|An indicator appears next to your name and avatar',
+ 'SetStatusModal|Displays that you are busy or not able to respond',
),
clearStatusAfterDropdownLabel: s__('SetStatusModal|Clear status after'),
clearStatusAfterMessage: s__('SetStatusModal|Your status resets on %{date}.'),
@@ -161,11 +161,7 @@ export default {
@click="handleEmojiClick"
>
<template #button-content>
- <span
- v-if="noEmoji"
- class="no-emoji-placeholder position-relative"
- data-testid="no-emoji-placeholder"
- >
+ <span v-if="noEmoji" class="gl-relative" data-testid="no-emoji-placeholder">
<gl-icon name="slight-smile" class="award-control-icon-neutral" />
<gl-icon name="smiley" class="award-control-icon-positive" />
<gl-icon name="smile" class="award-control-icon-super-positive" />
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index 80b1cb8c4d5..80158c55dbc 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -1,7 +1,7 @@
<script>
import { GlToast, GlTooltipDirective, GlSafeHtmlDirective, GlModal } from '@gitlab/ui';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api';
@@ -89,7 +89,7 @@ export default {
window.location.reload();
},
onUpdateFail() {
- createFlash({
+ createAlert({
message: s__(
"SetStatusModal|Sorry, we weren't able to set your status. Please try again later.",
),
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 18b26c7d8bd..15fd365b4da 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,6 +1,6 @@
<script>
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
@@ -113,7 +113,7 @@ export default {
})
.catch(() => {
this.loading = false;
- return createFlash({
+ return createAlert({
message: __('Error occurred when saving assignees'),
});
});
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 26fda2a823c..395dcf73693 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { __, n__ } from '~/locale';
import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
@@ -111,7 +111,7 @@ export default {
}
},
error() {
- createFlash({ message: __('An error occurred while fetching participants.') });
+ createAlert({ message: __('An error occurred while fetching participants.') });
},
},
},
@@ -191,7 +191,7 @@ export default {
return data;
})
.catch(() => {
- createFlash({ message: __('An error occurred while updating assignees.') });
+ createAlert({ message: __('An error occurred while updating assignees.') });
})
.finally(() => {
this.isSettingAssignees = false;
@@ -220,7 +220,7 @@ export default {
this.$refs.userSelect.showDropdown();
},
showError() {
- createFlash({ message: __('An error occurred while fetching participants.') });
+ createAlert({ message: __('An error occurred while fetching participants.') });
},
setDirtyState() {
this.isDirty = true;
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
index 0ed40f56bea..29298ef7627 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
@@ -58,7 +58,7 @@ export default {
v-if="hasCannotMergeIcon"
name="warning-solid"
aria-hidden="true"
- class="merge-icon gl-left-6 gl-bottom-0"
+ class="merge-icon"
:size="12"
/>
</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
index c44ce8b0057..3532b75b6e7 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -1,6 +1,6 @@
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import { confidentialityQueries } from '~/sidebar/constants';
@@ -92,7 +92,7 @@ export default {
},
}) => {
if (errors.length) {
- createFlash({
+ createAlert({
message: errors[0],
});
} else {
@@ -101,7 +101,7 @@ export default {
},
)
.catch(() => {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} confidentiality.'),
{
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
index f234c5ea3c9..f3bd58c11d4 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
@@ -1,7 +1,7 @@
<script>
import produce from 'immer';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { confidentialityQueries, Tracking } from '~/sidebar/constants';
@@ -72,7 +72,7 @@ export default {
this.$emit('confidentialityUpdated', data.workspace?.issuable?.confidential);
},
error() {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} confidentiality.'),
{
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
index 67f36f65b5d..81090bfa062 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
import getIssueCrmContactsQuery from './queries/get_issue_crm_contacts.query.graphql';
@@ -41,7 +41,7 @@ export default {
return data?.issue?.customerRelationsContacts?.nodes;
},
error(error) {
- createFlash({
+ createAlert({
message: __('Something went wrong trying to load issue contacts.'),
error,
captureError: true,
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
index ef99d540c86..98468583992 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlDatepicker, GlTooltipDirective, GlLink, GlPopover } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
@@ -92,7 +92,7 @@ export default {
this.$emit(`${this.dateType}Updated`, data.workspace?.issuable?.[this.dateType]);
},
error() {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} %{dateType} date.'),
{
@@ -205,7 +205,7 @@ export default {
},
}) => {
if (errors.length) {
- createFlash({
+ createAlert({
message: errors[0],
});
} else {
@@ -214,7 +214,7 @@ export default {
},
)
.catch(() => {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} %{dateType} date.'),
{
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index 8145506f32c..df03af346c0 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -2,7 +2,7 @@
import { GlButton } from '@gitlab/ui';
import $ from 'jquery';
import { mapActions } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import eventHub from '../../event_hub';
@@ -52,7 +52,7 @@ export default {
const flashMessage = __(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
);
- createFlash({
+ createAlert({
message: sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }),
});
})
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 286bd50f6dd..d32d8a7b044 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -3,7 +3,7 @@ import { GlIcon, GlTooltipDirective, GlOutsideDirective as Outside } from '@gitl
import { mapGetters, mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import eventHub from '~/sidebar/event_hub';
import toast from '~/vue_shared/plugins/global_toast';
import EditForm from './edit_form.vue';
@@ -95,7 +95,7 @@ export default {
const flashMessage = __(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
);
- createFlash({
+ createAlert({
message: sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }),
});
})
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
index 933b9b11b40..55bb214aa65 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -43,6 +43,7 @@ export default {
data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="reviewer"
+ data-qa-selector="reviewers_edit_button"
>
{{ __('Edit') }}
</a>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index b0d820ddd15..ad061dd2e6b 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -3,7 +3,7 @@
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import Vue from 'vue';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
@@ -64,7 +64,7 @@ export default {
this.initialLoading = false;
},
error() {
- createFlash({ message: __('An error occurred while fetching reviewers.') });
+ createAlert({ message: __('An error occurred while fetching reviewers.') });
},
},
},
@@ -85,7 +85,7 @@ export default {
return this.loading || this.$apollo.queries.issuable.loading;
},
canUpdate() {
- return this.issuable.userPermissions?.updateMergeRequest || false;
+ return this.issuable.userPermissions?.adminMergeRequest || false;
},
},
created() {
@@ -120,7 +120,7 @@ export default {
})
.catch(() => {
this.loading = false;
- return createFlash({
+ return createAlert({
message: __('Error occurred when saving reviewers'),
});
});
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
index a562df4ecd6..f02e0c783e1 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -7,7 +7,7 @@ import {
GlSprintf,
GlButton,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
import updateIssuableSeverity from './graphql/mutations/update_issuable_severity.mutation.graphql';
import SeverityToken from './severity.vue';
@@ -123,7 +123,7 @@ export default {
this.severity = severity;
})
.catch(() =>
- createFlash({
+ createAlert({
message: `${this.$options.i18n.UPDATE_SEVERITY_ERROR} ${this.$options.i18n.TRY_AGAIN}`,
}),
)
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 6c615109bb8..c33b1468ca4 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -13,7 +13,7 @@ import {
GlButton,
} from '@gitlab/ui';
import { kebabCase, snakeCase } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
@@ -25,6 +25,8 @@ import {
Tracking,
IssuableAttributeState,
IssuableAttributeType,
+ LocalizedIssuableAttributeType,
+ IssuableAttributeTypeKeyMap,
issuableAttributesQueries,
noAttributeId,
defaultEpicSort,
@@ -125,7 +127,7 @@ export default {
return data?.workspace?.issuable.attribute;
},
error(error) {
- createFlash({
+ createAlert({
message: this.i18n.currentFetchError,
captureError: true,
error,
@@ -179,7 +181,7 @@ export default {
return [];
},
error(error) {
- createFlash({ message: this.i18n.listFetchError, captureError: true, error });
+ createAlert({ message: this.i18n.listFetchError, captureError: true, error });
},
},
},
@@ -229,7 +231,9 @@ export default {
return timeFor(this.currentAttribute?.dueDate);
},
i18n() {
- return dropdowni18nText(this.issuableAttribute, this.issuableType);
+ const localizedAttribute =
+ LocalizedIssuableAttributeType[IssuableAttributeTypeKeyMap[this.issuableAttribute]];
+ return dropdowni18nText(localizedAttribute, this.issuableType);
},
isEpic() {
// MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
@@ -280,7 +284,7 @@ export default {
})
.then(({ data }) => {
if (data.issuableSetAttribute?.errors?.length) {
- createFlash({
+ createAlert({
message: data.issuableSetAttribute.errors[0],
captureError: true,
error: data.issuableSetAttribute.errors[0],
@@ -290,7 +294,7 @@ export default {
}
})
.catch((error) => {
- createFlash({ message: this.i18n.updateError, captureError: true, error });
+ createAlert({ message: this.i18n.updateError, captureError: true, error });
})
.finally(() => {
this.updating = false;
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index cc88812c7b0..1680e42e5e4 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -89,7 +89,9 @@ export default {
return;
}
- this.edit = true;
+ if (this.canEdit && this.canUpdate) {
+ this.edit = true;
+ }
this.$emit('open');
window.addEventListener('click', this.collapseWhenOffClick);
window.addEventListener('keyup', this.collapseOnEscape);
@@ -125,7 +127,7 @@ export default {
<template>
<div>
<div
- class="gl-display-flex gl-align-items-center gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold"
+ class="gl-display-flex gl-align-items-center gl-line-height-20 gl-text-gray-900 gl-font-weight-bold"
@click.self="collapse"
>
<span class="hide-collapsed" data-testid="title" @click="collapse">
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index e5bee4df9b8..99e7c825b72 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -1,6 +1,6 @@
<script>
-import { GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
@@ -22,6 +22,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
+ GlDropdownForm,
GlIcon,
GlLoadingIcon,
GlToggle,
@@ -73,7 +74,7 @@ export default {
this.$emit('subscribedUpdated', data.workspace?.issuable?.subscribed);
},
error() {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} notifications.'),
{
@@ -137,7 +138,7 @@ export default {
},
}) => {
if (errors.length) {
- createFlash({
+ createAlert({
message: errors[0],
});
}
@@ -148,7 +149,7 @@ export default {
},
)
.catch(() => {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} notifications.'),
{
@@ -181,7 +182,7 @@ export default {
</script>
<template>
- <div v-if="isMergeRequest" class="gl-new-dropdown-item">
+ <gl-dropdown-form v-if="isMergeRequest" class="gl-new-dropdown-item">
<div class="gl-px-5 gl-pb-2 gl-pt-1">
<gl-toggle
:value="subscribed"
@@ -192,7 +193,7 @@ export default {
@change="toggleSubscribed"
/>
</div>
- </div>
+ </gl-dropdown-form>
<sidebar-editable-item
v-else
ref="editable"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index d751816bd94..124464088cf 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlTableLite, GlButton, GlTooltipDirective } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
@@ -47,7 +47,7 @@ export default {
return this.extractTimelogs(data);
},
error() {
- createFlash({ message: __('Something went wrong. Please try again.') });
+ createAlert({ message: __('Something went wrong. Please try again.') });
},
},
},
@@ -105,7 +105,7 @@ export default {
}
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__('TimeTracking|An error occurred while removing the timelog.'),
captureError: true,
error,
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index d472b67d976..62b05421884 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -61,7 +61,7 @@ export default {
</script>
<template>
- <div class="block">
+ <div class="block time-tracking">
<issuable-time-tracker
:full-path="fullPath"
:issuable-id="issuableId"
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 42e16aae312..5da2d65723a 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
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { produce } from 'immer';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
import { todoLabel } from '~/vue_shared/components/sidebar/todo_toggle//utils';
@@ -73,7 +73,7 @@ export default {
this.$emit('todoUpdated', currentUserTodos.length > 0);
},
error() {
- createFlash({
+ createAlert({
message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
issuableType: this.issuableType,
}),
@@ -155,7 +155,7 @@ export default {
},
}) => {
if (errors.length) {
- createFlash({
+ createAlert({
message: errors[0],
});
}
@@ -166,7 +166,7 @@ export default {
},
)
.catch(() => {
- createFlash({
+ createAlert({
message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
issuableType: this.issuableType,
}),
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 60cb4cff727..6248bcb8e2d 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,3 +1,4 @@
+import { invert } from 'lodash';
import { s__, __, sprintf } from '~/locale';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
@@ -251,6 +252,12 @@ export const IssuableAttributeType = {
Milestone: 'milestone',
};
+export const LocalizedIssuableAttributeType = {
+ Milestone: s__('Issuable|milestone'),
+};
+
+export const IssuableAttributeTypeKeyMap = invert(IssuableAttributeType);
+
export const IssuableAttributeState = {
[IssuableAttributeType.Milestone]: 'active',
};
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 5a3122e83d0..2cce27df598 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { escape } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
function isValidProjectId(id) {
@@ -44,7 +44,7 @@ class SidebarMoveIssue {
.fetchAutocompleteProjects(searchTerm)
.then(callback)
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while fetching projects autocomplete.'),
}),
);
@@ -79,7 +79,7 @@ class SidebarMoveIssue {
this.$confirmButton.disable().addClass('is-loading');
this.mediator.moveIssue().catch(() => {
- createFlash({ message: __('An error occurred while moving the issue.') });
+ createAlert({ message: __('An error occurred while moving the issue.') });
this.$confirmButton.enable().removeClass('is-loading');
});
}
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 1cb3c30b9e0..9b5bad710dd 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -161,7 +161,7 @@ function mountAssigneesComponent() {
fullPath,
issuableType,
issuableId: id,
- allowMultipleAssignees: !el.dataset.maxAssignees,
+ allowMultipleAssignees: !el.dataset.maxAssignees || el.dataset.maxAssignees > 1,
editable,
},
scopedSlots: {
diff --git a/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql b/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
index d665ca1e084..71ce58fb9cc 100644
--- a/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
@@ -1,5 +1,4 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
-#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query epicParticipants($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
@@ -9,7 +8,6 @@ query epicParticipants($fullPath: ID!, $iid: ID) {
participants {
nodes {
...User
- ...UserAvailability
}
}
}
diff --git a/app/assets/javascripts/sidebar/queries/sidebar_details.query.graphql b/app/assets/javascripts/sidebar/queries/sidebar_details.query.graphql
deleted file mode 100644
index 90d1a7794ea..00000000000
--- a/app/assets/javascripts/sidebar/queries/sidebar_details.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-query sidebarDetails($fullPath: ID!, $iid: String!) {
- project(fullPath: $fullPath) {
- id
- issue(iid: $iid) {
- id
- iid
- }
- }
-}
diff --git a/app/assets/javascripts/sidebar/queries/sidebar_details_mr.query.graphql b/app/assets/javascripts/sidebar/queries/sidebar_details_mr.query.graphql
deleted file mode 100644
index 0505f88773d..00000000000
--- a/app/assets/javascripts/sidebar/queries/sidebar_details_mr.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-query mergeRequestSidebarDetails($fullPath: ID!, $iid: String!) {
- project(fullPath: $fullPath) {
- id
- mergeRequest(iid: $iid) {
- id
- iid # currently unused.
- }
- }
-}
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index beacdeb559c..00d3177b75a 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -1,15 +1,8 @@
-import sidebarDetailsIssueQuery from 'ee_else_ce/sidebar/queries/sidebar_details.query.graphql';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
-import sidebarDetailsMRQuery from '../queries/sidebar_details_mr.query.graphql';
-
-const queries = {
- merge_request: sidebarDetailsMRQuery,
- issue: sidebarDetailsIssueQuery,
-};
export const gqClient = createGqClient(
{},
@@ -36,37 +29,13 @@ export default class SidebarService {
}
get() {
- return Promise.all([
- axios.get(this.endpoint),
- gqClient.query({
- query: this.sidebarDetailsQuery(),
- variables: {
- fullPath: this.fullPath,
- iid: this.iid.toString(),
- },
- }),
- ]);
- }
-
- sidebarDetailsQuery() {
- return queries[this.issuableType];
+ return axios.get(this.endpoint);
}
update(key, data) {
return axios.put(this.endpoint, { [key]: data });
}
- updateWithGraphQl(mutation, variables) {
- return gqClient.mutate({
- mutation,
- variables: {
- ...variables,
- projectPath: this.fullPath,
- iid: this.iid.toString(),
- },
- });
- }
-
getProjectsAutocomplete(searchTerm) {
return axios.get(this.projectsAutocompleteEndpoint, {
params: {
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index f7c93b6903c..912f0fdcbef 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,5 +1,5 @@
import Store from '~/sidebar/stores/sidebar_store';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { visitUrl } from '../lib/utils/url_utility';
@@ -93,11 +93,11 @@ export default class SidebarMediator {
fetch() {
return this.service
.get()
- .then(([restResponse, graphQlResponse]) => {
- this.processFetchedData(restResponse.data, graphQlResponse.data);
+ .then(({ data }) => {
+ this.processFetchedData(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error occurred when fetching sidebar data'),
}),
);
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 26838682fc8..6e5b2ce4dbe 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,10 +1,10 @@
/* eslint-disable consistent-return */
import $ from 'jquery';
+import { createAlert } from '~/flash';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
import { spriteIcon } from '~/lib/utils/common_utils';
import FilesCommentButton from './files_comment_button';
-import createFlash from './flash';
import initImageDiffHelper from './image_diff/helpers/init_image_diff';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
@@ -96,7 +96,7 @@ export default class SingleFileDiff {
if (cb) cb();
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('An error occurred while retrieving diff'),
});
});
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 2537ec78850..4a7528d9c8e 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -2,7 +2,7 @@
import { GlButton, GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
import eventHub from '~/blob/components/eventhub';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import {
@@ -145,7 +145,7 @@ export default {
const defaultErrorMsg = this.newSnippet
? SNIPPET_CREATE_MUTATION_ERROR
: SNIPPET_UPDATE_MUTATION_ERROR;
- createFlash({
+ createAlert({
message: sprintf(defaultErrorMsg, { err }),
});
this.isUpdating = false;
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index fe169775f96..7e80928cbea 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
import { sprintf } from '~/locale';
@@ -63,7 +63,7 @@ export default {
.catch((e) => this.flashAPIFailure(e));
},
flashAPIFailure(err) {
- createFlash({ message: sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }) });
+ createAlert({ message: sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }) });
},
},
};
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index 86cbc2c31b3..360ffdd34e0 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -53,7 +53,9 @@ export default {
return {
blobContent: '',
activeViewerType:
- this.blob?.richViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
+ this.blob?.richViewer && !window.location.hash?.startsWith('#LC')
+ ? RICH_BLOB_VIEWER
+ : SIMPLE_BLOB_VIEWER,
};
},
computed: {
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index dd8f2897018..759a3f31a05 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -19,7 +19,7 @@ import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/flash';
import DeleteSnippetMutation from '../mutations/delete_snippet.mutation.graphql';
@@ -196,12 +196,12 @@ export default {
try {
this.isSubmittingSpam = true;
await axios.post(this.reportAbusePath);
- createFlash({
+ createAlert({
message: this.$options.i18n.snippetSpamSuccess,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
} catch (error) {
- createFlash({ message: this.$options.i18n.snippetSpamFailure });
+ createAlert({ message: this.$options.i18n.snippetSpamFailure, variant: VARIANT_DANGER });
} finally {
this.isSubmittingSpam = false;
}
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index 6e72d95c8e6..a7760ad5d0b 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import 'deckar01-task_list';
import { __ } from '~/locale';
-import createFlash from './flash';
+import { createAlert } from '~/flash';
import axios from './lib/utils/axios_utils';
export default class TaskList {
@@ -23,7 +23,7 @@ export default class TaskList {
errorMessages = e.response.data.errors.join(' ');
}
- return createFlash({
+ return createAlert({
message: errorMessages || __('Update failed'),
});
};
diff --git a/app/assets/javascripts/token_access/components/token_access.vue b/app/assets/javascripts/token_access/components/token_access.vue
index 363a9d58d65..4b91872d80d 100644
--- a/app/assets/javascripts/token_access/components/token_access.vue
+++ b/app/assets/javascripts/token_access/components/token_access.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlAlert,
GlButton,
GlCard,
GlFormInput,
@@ -8,7 +9,7 @@ import {
GlSprintf,
GlToggle,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import addProjectCIJobTokenScopeMutation from '../graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
@@ -25,6 +26,9 @@ export default {
`CICD|Select the projects that can be accessed by API requests authenticated with this project's CI_JOB_TOKEN CI/CD variable. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more.%{linkEnd}`,
),
cardHeaderTitle: s__('CICD|Add an existing project to the scope'),
+ settingDisabledMessage: s__(
+ 'CICD|Enable feature to limit job token access to the following projects.',
+ ),
addProject: __('Add project'),
cancel: __('Cancel'),
addProjectPlaceholder: __('Paste project path (i.e. gitlab-org/gitlab)'),
@@ -32,6 +36,7 @@ export default {
scopeFetchError: __('There was a problem fetching the job token scope value'),
},
components: {
+ GlAlert,
GlButton,
GlCard,
GlFormInput,
@@ -58,7 +63,7 @@ export default {
return data.project.ciCdSettings.jobTokenScopeEnabled;
},
error() {
- createFlash({ message: this.$options.i18n.scopeFetchError });
+ createAlert({ message: this.$options.i18n.scopeFetchError });
},
},
projects: {
@@ -72,7 +77,7 @@ export default {
return data.project?.ciJobTokenScope?.projects?.nodes ?? [];
},
error() {
- createFlash({ message: this.$options.i18n.projectsFetchError });
+ createAlert({ message: this.$options.i18n.projectsFetchError });
},
},
},
@@ -112,7 +117,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- createFlash({ message: error });
+ createAlert({ message: error });
}
},
async addProject() {
@@ -135,7 +140,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- createFlash({ message: error });
+ createAlert({ message: error });
} finally {
this.clearTargetProjectPath();
this.getProjects();
@@ -161,7 +166,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- createFlash({ message: error });
+ createAlert({ message: error });
} finally {
this.getProjects();
}
@@ -195,8 +200,8 @@ export default {
</template>
</gl-toggle>
- <div data-testid="token-section">
- <gl-card class="gl-mt-5">
+ <div>
+ <gl-card class="gl-mt-5 gl-mb-3">
<template #header>
<h5 class="gl-my-0">{{ $options.i18n.cardHeaderTitle }}</h5>
</template>
@@ -213,7 +218,16 @@ export default {
<gl-button @click="clearTargetProjectPath">{{ $options.i18n.cancel }}</gl-button>
</template>
</gl-card>
-
+ <gl-alert
+ v-if="!jobTokenScopeEnabled"
+ class="gl-mb-3"
+ variant="warning"
+ :dismissible="false"
+ :show-icon="false"
+ data-testid="token-disabled-alert"
+ >
+ {{ $options.i18n.settingDisabledMessage }}
+ </gl-alert>
<token-projects-table :projects="projects" @removeProject="removeProject" />
</div>
</template>
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index 94b4ee77e7e..bd425bdc2a8 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -50,6 +50,7 @@ function UsersSelect(currentUser, els, options = {}) {
options.iid = $dropdown.data('iid');
options.issuableType = $dropdown.data('issuableType');
options.targetBranch = $dropdown.data('targetBranch');
+ options.showSuggested = $dropdown.data('showSuggested');
const showNullUser = $dropdown.data('nullUser');
const defaultNullUser = $dropdown.data('nullUserDefault');
const showMenuAbove = $dropdown.data('showMenuAbove');
@@ -340,6 +341,16 @@ function UsersSelect(currentUser, els, options = {}) {
if ($dropdown.hasClass('js-multiselect')) {
const selected = getSelected().filter((i) => i !== 0);
+ if ($dropdown.data('showSuggested')) {
+ const suggested = this.suggestedUsers(users);
+ if (suggested.length) {
+ users = users.filter(
+ (u) => !u.suggested || (u.suggested && selected.indexOf(u.id) !== -1),
+ );
+ users.splice(showDivider + 1, 0, ...suggested);
+ }
+ }
+
if (selected.length > 0) {
if ($dropdown.data('dropdownHeader')) {
showDivider += 1;
@@ -370,6 +381,19 @@ function UsersSelect(currentUser, els, options = {}) {
$dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
}
},
+ suggestedUsers(users) {
+ const selected = getSelected().filter((i) => i !== 0);
+ const suggestedUsers = users.filter((u) => u.suggested && selected.indexOf(u.id) === -1);
+
+ if (!suggestedUsers.length) return [];
+
+ const items = [
+ { type: 'header', content: $dropdown.data('suggestedReviewersHeader') },
+ ...suggestedUsers,
+ { type: 'header', content: $dropdown.data('allMembersHeader') },
+ ];
+ return items;
+ },
filterable: true,
filterRemote: true,
search: {
@@ -760,6 +784,10 @@ UsersSelect.prototype.users = function (query, options, callback) {
params.approval_rules = true;
}
+ if (isMergeRequest && options.showSuggested) {
+ params.show_suggested = true;
+ }
+
if (isNewMergeRequest) {
params.target_branch = options.targetBranch || null;
}
@@ -791,13 +819,14 @@ UsersSelect.prototype.renderRow = function (
const tooltipAttributes = tooltip
? `data-container="body" data-placement="left" data-title="${tooltip}"`
: '';
+ const dataUserSuggested = user.suggested ? `data-user-suggested=${user.suggested}` : '';
const name =
user?.availability && isUserBusy(user.availability)
? sprintf(__('%{name} (Busy)'), { name: user.name })
: user.name;
return `
- <li data-user-id=${user.id}>
+ <li data-user-id=${user.id} ${dataUserSuggested}>
<a href="#" class="dropdown-menu-user-link gl-display-flex! gl-align-items-center ${linkClasses}" ${tooltipAttributes}>
${this.renderRowAvatar(issuableType, user, img)}
<span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
index 30a0e7c383c..5339d7faf85 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
@@ -74,9 +74,11 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-flex gl-align-items-flex-start">
<gl-dropdown
v-if="tertiaryButtons.length"
+ v-gl-tooltip
+ :title="__('Options')"
:text="dropdownLabel"
icon="ellipsis_v"
no-caret
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
index f782c28ea19..2cfeb7a4bcb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__, __ } from '~/locale';
@@ -139,7 +139,7 @@ export default {
this.fetchingApprovals = false;
})
.catch(() =>
- createFlash({
+ createAlert({
message: FETCH_ERROR,
}),
);
@@ -154,7 +154,7 @@ export default {
this.updateApproval(
() => this.service.approveMergeRequest(),
() =>
- createFlash({
+ createAlert({
message: APPROVE_ERROR,
}),
);
@@ -167,7 +167,7 @@ export default {
this.hasApprovalAuthError = true;
return;
}
- createFlash({
+ createAlert({
message: APPROVE_ERROR,
});
},
@@ -177,7 +177,7 @@ export default {
this.updateApproval(
() => this.service.unapproveMergeRequest(),
() =>
- createFlash({
+ createAlert({
message: UNAPPROVE_ERROR,
}),
);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
index 7ba387c79b1..d6d1cae4029 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
@@ -1,9 +1,10 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import eventHub from '../../event_hub';
import MRWidgetService from '../../services/mr_widget_service';
import {
MANUAL_DEPLOY,
@@ -129,11 +130,12 @@ export default {
}
})
.catch(() => {
- createFlash({
+ createAlert({
message: errorMessage,
});
})
.finally(() => {
+ eventHub.$emit('FetchDeployments');
this.actionInProgress = null;
});
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index 1e363b0f5fb..5efa0e2879e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -7,7 +7,10 @@ import {
GlLink,
GlSearchBoxByType,
} from '@gitlab/ui';
+import { isSafeURL } from '~/lib/utils/url_utility';
+import { s__, __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import ReviewAppLink from '../review_app_link.vue';
export default {
@@ -19,6 +22,7 @@ export default {
GlIcon,
GlLink,
GlSearchBoxByType,
+ ModalCopyButton,
ReviewAppLink,
},
directives: {
@@ -50,6 +54,13 @@ export default {
filteredChanges() {
return this.deployment?.changes?.filter((change) => change.path.includes(this.searchTerm));
},
+ isSafeUrl() {
+ return isSafeURL(this.deploymentExternalUrl);
+ },
+ },
+ i18n: {
+ copy: __('Copy URL'),
+ copyTitle: s__('Environments|Copy live environment URL'),
},
};
</script>
@@ -57,11 +68,20 @@ export default {
<span class="gl-display-inline-flex">
<gl-button-group v-if="shouldRenderDropdown" size="small">
<review-app-link
+ v-if="isSafeUrl"
:display="appButtonText"
:link="deploymentExternalUrl"
size="small"
css-class="deploy-link js-deploy-url inline gl-ml-3"
/>
+ <modal-copy-button
+ v-else
+ :title="$options.i18n.copyTitle"
+ :text="deploymentExternalUrl"
+ size="small"
+ >
+ {{ $options.i18n.copy }}
+ </modal-copy-button>
<gl-dropdown toggle-class="gl-px-2!" size="small" class="js-mr-wigdet-deployment-dropdown">
<template #button-content>
<gl-icon
@@ -90,12 +110,22 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</gl-button-group>
- <review-app-link
- v-else
- :display="appButtonText"
- :link="deploymentExternalUrl"
- size="small"
- css-class="js-deploy-url deploy-link btn btn-default btn-sm inline gl-ml-3"
- />
+ <template v-else>
+ <review-app-link
+ v-if="isSafeUrl"
+ :display="appButtonText"
+ :link="deploymentExternalUrl"
+ size="small"
+ css-class="deploy-link js-deploy-url inline gl-ml-3"
+ />
+ <modal-copy-button
+ v-else
+ :title="$options.i18n.copyTitle"
+ :text="deploymentExternalUrl"
+ size="small"
+ >
+ {{ $options.i18n.copy }}
+ </modal-copy-button>
+ </template>
</span>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 300e2a672cb..3d03dbd9db3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -315,7 +315,6 @@ export default {
data-qa-selector="mr_widget_extension"
>
<state-container
- :mr="mr"
:status="statusIconName"
:is-loading="isLoadingSummary"
:class="{ 'gl-cursor-pointer': isCollapsible }"
@@ -324,7 +323,7 @@ export default {
@mouseup="onRowMouseUp"
>
<div
- class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
+ class="media-body gl-display-flex gl-flex-direction-row! gl-w-full"
data-testid="widget-extension-top-level"
>
<div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
index d67ff11f297..e3f87c08ad4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
@@ -28,7 +28,7 @@ const nonStandardEvents = {
},
counter: {},
},
- testReport: {
+ testSummary: {
uniqueUser: {
expand: ['i_testing_summary_widget_total'],
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
index 94a1b805b99..870972156c5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
@@ -26,16 +26,6 @@ export default {
required: false,
default: true,
},
- divergedCommitsCount: {
- type: Number,
- required: false,
- default: 0,
- },
- targetBranchPath: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
closesText() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
index 822c5a68093..932659f3c89 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
@@ -16,7 +16,8 @@ export default {
props: {
mr: {
type: Object,
- required: true,
+ required: false,
+ default: null,
},
isLoading: {
type: Boolean,
@@ -80,6 +81,7 @@ export default {
</div>
</div>
<div
+ v-if="mr"
class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
>
<gl-button
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
index e2a9caf5419..2b22033514f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
@@ -1,6 +1,7 @@
<script>
import { s__ } from '~/locale';
import StatusIcon from '../mr_widget_status_icon.vue';
+import { DETAILED_MERGE_STATUS } from '../../constants';
export default {
i18n: {
@@ -22,7 +23,7 @@ export default {
failedText() {
if (this.mr.approvals && !this.mr.isApproved) {
return this.$options.i18n.approvalNeeded;
- } else if (this.mr.blockingMergeRequests?.total_count > 0) {
+ } else if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.BLOCKED_STATUS) {
return this.$options.i18n.blockingMergeRequests;
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 3c6c2a44e70..92a7fa39cdc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -2,7 +2,7 @@
import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
@@ -113,7 +113,7 @@ export default {
})
.catch(() => {
this.isCancellingAutoMerge = false;
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
});
@@ -141,7 +141,7 @@ export default {
})
.catch(() => {
this.isRemovingSourceBranch = false;
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index e9298b0c856..46392565088 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__, __ } from '~/locale';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
import modalEventHub from '~/projects/commit/event_hub';
@@ -131,7 +131,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
});
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 37c8d5d15f3..f6843c1f3d3 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
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import toast from '~/vue_shared/plugins/global_toast';
@@ -111,7 +111,7 @@ export default {
if (error.response && error.response.data && error.response.data.merge_error) {
this.rebasingError = error.response.data.merge_error;
} else {
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
}
@@ -142,7 +142,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
stopPolling();
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
index 3cbd171a035..853895a4296 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -11,6 +11,12 @@ export default {
GlSprintf,
StatusIcon,
},
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
computed: {
troubleshootingDocsPath() {
return helpPagePath('ci/troubleshooting', { anchor: 'merge-request-status-messages' });
@@ -29,7 +35,14 @@ export default {
<status-icon status="failed" />
<div class="media-body space-children">
<span class="gl-font-weight-bold">
- <gl-sprintf :message="$options.i18n.failedMessage">
+ <span v-if="mr.isPipelineBlocked">
+ {{
+ s__(
+ `mrWidget|Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.`,
+ )
+ }}
+ </span>
+ <gl-sprintf v-else :message="$options.i18n.failedMessage">
<template #link="{ content }">
<gl-link :href="troubleshootingDocsPath" target="_blank">
{{ content }}
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 78430abcfe9..1298c1316e2 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
@@ -14,10 +14,10 @@ import {
import { isEmpty } from 'lodash';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import simplePoll from '~/lib/utils/simple_poll';
-import { __, s__ } from '~/locale';
+import { __, s__, n__ } from '~/locale';
import SmartInterval from '~/smart_interval';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -325,15 +325,20 @@ export default {
);
},
sourceBranchDeletedText() {
- if (this.removeSourceBranch) {
- return this.mr.state === 'merged'
- ? __('Deleted the source branch.')
- : __('Source branch will be deleted.');
+ const isPreMerge = this.mr.state !== 'merged';
+
+ if (isPreMerge) {
+ return this.mr.shouldRemoveSourceBranch
+ ? __('Source branch will be deleted.')
+ : __('Source branch will not be deleted.');
}
- return this.mr.state === 'merged'
- ? __('Did not delete the source branch.')
- : __('Source branch will not be deleted.');
+ return this.mr.sourceBranchRemoved
+ ? __('Deleted the source branch.')
+ : __('Did not delete the source branch.');
+ },
+ sourceHasDivergedFromTarget() {
+ return this.mr.divergedCommitsCount > 0;
},
showMergeDetailsHeader() {
return ['readyToMerge'].indexOf(this.mr.state) >= 0;
@@ -439,7 +444,7 @@ export default {
.catch(() => {
this.isMakingRequest = false;
this.mr.transitionStateMachine({ transition: MERGE_FAILURE });
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
});
@@ -483,7 +488,7 @@ export default {
}
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong while deleting the source branch. Please try again.'),
});
});
@@ -507,6 +512,8 @@ export default {
mergeAndSquashCommitTemplatesHintText: s__(
'mrWidget|To change these default messages, edit the templates for both the merge and squash commit messages. %{linkStart}Learn more.%{linkEnd}',
),
+ sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch'),
+ divergedCommits: (count) => n__('%d commit behind', '%d commits behind', count),
},
};
</script>
@@ -530,130 +537,148 @@ export default {
<div class="mr-widget-body mr-widget-body-ready-merge media mr-widget-body-line-height-1">
<div class="media-body">
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
- <gl-button-group v-if="shouldShowMergeControls" class="gl-align-self-start">
- <gl-button
- size="medium"
- category="primary"
- class="accept-merge-request"
- data-testid="merge-button"
- variant="confirm"
- :disabled="isMergeButtonDisabled"
- :loading="isMakingRequest"
- data-qa-selector="merge_button"
- @click="handleMergeButtonClick(isAutoMergeAvailable)"
- >{{ mergeButtonText }}</gl-button
- >
- <gl-dropdown
- v-if="shouldShowMergeImmediatelyDropdown"
- v-gl-tooltip.hover.focus="__('Select merge moment')"
- :disabled="isMergeButtonDisabled"
- variant="confirm"
- data-qa-selector="merge_moment_dropdown"
- toggle-class="btn-icon js-merge-moment"
- >
- <template #button-content>
- <gl-icon name="chevron-down" class="mr-0" />
- <span class="sr-only">{{ __('Select merge moment') }}</span>
- </template>
- <gl-dropdown-item
- icon-name="warning"
- button-class="accept-merge-request js-merge-immediately-button"
- data-qa-selector="merge_immediately_menu_item"
- @click="handleMergeImmediatelyButtonClick"
+ <template v-if="shouldShowMergeControls">
+ <div class="gl-display-flex gl-align-items-center gl-flex-wrap gl-w-full gl-mb-5">
+ <gl-form-checkbox
+ v-if="canRemoveSourceBranch"
+ id="remove-source-branch-input"
+ v-model="removeSourceBranch"
+ :disabled="isRemoveSourceBranchButtonDisabled"
+ class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center gl-mr-5"
>
- {{ __('Merge immediately') }}
- </gl-dropdown-item>
- <merge-immediately-confirmation-dialog
- ref="confirmationDialog"
- :docs-url="mr.mergeImmediatelyDocsPath"
- @mergeImmediately="onMergeImmediatelyConfirmation"
+ {{ __('Delete source branch') }}
+ </gl-form-checkbox>
+
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ v-model="squashBeforeMerge"
+ :help-path="mr.squashBeforeMergeHelpPath"
+ :is-disabled="isSquashReadOnly"
+ class="gl-mr-5"
/>
- </gl-dropdown>
- <merge-train-failed-pipeline-confirmation-dialog
- :visible="isPipelineFailedModalVisibleMergeTrain"
- @startMergeTrain="onStartMergeTrainConfirmation"
- @cancel="isPipelineFailedModalVisibleMergeTrain = false"
- />
- <merge-failed-pipeline-confirmation-dialog
- :visible="isPipelineFailedModalVisibleNormalMerge"
- @mergeWithFailedPipeline="onMergeWithFailedPipelineConfirmation"
- @cancel="isPipelineFailedModalVisibleNormalMerge = false"
- />
- </gl-button-group>
- <merge-train-helper-icon v-if="shouldRenderMergeTrainHelperIcon" class="gl-mx-3" />
- <div
- v-if="shouldShowMergeControls"
- class="gl-display-flex gl-align-items-center gl-flex-wrap gl-w-full gl-order-n1 gl-mb-5"
- >
- <gl-form-checkbox
- v-if="canRemoveSourceBranch"
- id="remove-source-branch-input"
- v-model="removeSourceBranch"
- :disabled="isRemoveSourceBranchButtonDisabled"
- class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center gl-mr-5"
- >
- {{ __('Delete source branch') }}
- </gl-form-checkbox>
-
- <!-- Placeholder for EE extension of this component -->
- <squash-before-merge
- v-if="shouldShowSquashBeforeMerge"
- v-model="squashBeforeMerge"
- :help-path="mr.squashBeforeMergeHelpPath"
- :is-disabled="isSquashReadOnly"
- class="gl-mr-5"
- />
-
- <gl-form-checkbox
- v-if="shouldShowSquashEdit || shouldShowMergeEdit"
- v-model="editCommitMessage"
- data-testid="widget_edit_commit_message"
- class="gl-display-flex gl-align-items-center"
- >
- {{ __('Edit commit message') }}
- </gl-form-checkbox>
- </div>
- <div
- v-if="editCommitMessage"
- class="gl-w-full gl-order-n1"
- data-testid="edit_commit_message"
- >
- <ul class="border-top commits-list flex-list gl-list-style-none gl-p-0 gl-pt-4">
- <commit-edit
- v-if="shouldShowSquashEdit"
- :value="squashCommitMessage"
- :label="__('Squash commit message')"
- input-id="squash-message-edit"
- class="gl-m-0! gl-p-0!"
- @input="setSquashCommitMessage"
+
+ <gl-form-checkbox
+ v-if="shouldShowSquashEdit || shouldShowMergeEdit"
+ v-model="editCommitMessage"
+ data-testid="widget_edit_commit_message"
+ class="gl-display-flex gl-align-items-center"
>
- <template #header>
- <commit-message-dropdown :commits="commits" @input="setSquashCommitMessage" />
+ {{ __('Edit commit message') }}
+ </gl-form-checkbox>
+ </div>
+ <div class="gl-w-full gl-text-gray-500 gl-mb-5">
+ <added-commit-message
+ :is-squash-enabled="squashBeforeMerge"
+ :is-fast-forward-enabled="!shouldShowMergeEdit"
+ :commits-count="commitsCount"
+ :target-branch="stateData.targetBranch"
+ />
+ <template v-if="mr.relatedLinks">
+ &middot;
+ <related-links
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ :show-assign-to-me="false"
+ :diverged-commits-count="mr.divergedCommitsCount"
+ :target-branch-path="mr.targetBranchPath"
+ class="mr-ready-merge-related-links gl-display-inline"
+ />
+ </template>
+ </div>
+ <div v-if="editCommitMessage" class="gl-w-full" data-testid="edit_commit_message">
+ <ul class="border-top commits-list flex-list gl-list-style-none gl-p-0 gl-pt-4">
+ <commit-edit
+ v-if="shouldShowSquashEdit"
+ :value="squashCommitMessage"
+ :label="__('Squash commit message')"
+ input-id="squash-message-edit"
+ class="gl-m-0! gl-p-0!"
+ @input="setSquashCommitMessage"
+ >
+ <template #header>
+ <commit-message-dropdown :commits="commits" @input="setSquashCommitMessage" />
+ </template>
+ </commit-edit>
+ <commit-edit
+ v-if="shouldShowMergeEdit"
+ :value="commitMessage"
+ :label="__('Merge commit message')"
+ input-id="merge-message-edit"
+ class="gl-m-0! gl-p-0!"
+ @input="setCommitMessage"
+ />
+ <li class="gl-m-0! gl-p-0!">
+ <p class="form-text text-muted">
+ <gl-sprintf :message="commitTemplateHintText">
+ <template #link="{ content }">
+ <gl-link
+ :href="commitTemplateHelpPage"
+ class="inline-link"
+ target="_blank"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </li>
+ </ul>
+ </div>
+ <gl-button-group class="gl-align-self-start">
+ <gl-button
+ size="medium"
+ category="primary"
+ class="accept-merge-request"
+ data-testid="merge-button"
+ variant="confirm"
+ :disabled="isMergeButtonDisabled"
+ :loading="isMakingRequest"
+ data-qa-selector="merge_button"
+ @click="handleMergeButtonClick(isAutoMergeAvailable)"
+ >{{ mergeButtonText }}</gl-button
+ >
+ <gl-dropdown
+ v-if="shouldShowMergeImmediatelyDropdown"
+ v-gl-tooltip.hover.focus="__('Select merge moment')"
+ :disabled="isMergeButtonDisabled"
+ variant="confirm"
+ data-qa-selector="merge_moment_dropdown"
+ toggle-class="btn-icon js-merge-moment"
+ >
+ <template #button-content>
+ <gl-icon name="chevron-down" class="mr-0" />
+ <span class="sr-only">{{ __('Select merge moment') }}</span>
</template>
- </commit-edit>
- <commit-edit
- v-if="shouldShowMergeEdit"
- :value="commitMessage"
- :label="__('Merge commit message')"
- input-id="merge-message-edit"
- class="gl-m-0! gl-p-0!"
- @input="setCommitMessage"
+ <gl-dropdown-item
+ icon-name="warning"
+ button-class="accept-merge-request js-merge-immediately-button"
+ data-qa-selector="merge_immediately_menu_item"
+ @click="handleMergeImmediatelyButtonClick"
+ >
+ {{ __('Merge immediately') }}
+ </gl-dropdown-item>
+ <merge-immediately-confirmation-dialog
+ ref="confirmationDialog"
+ :docs-url="mr.mergeImmediatelyDocsPath"
+ @mergeImmediately="onMergeImmediatelyConfirmation"
+ />
+ </gl-dropdown>
+ <merge-train-failed-pipeline-confirmation-dialog
+ :visible="isPipelineFailedModalVisibleMergeTrain"
+ @startMergeTrain="onStartMergeTrainConfirmation"
+ @cancel="isPipelineFailedModalVisibleMergeTrain = false"
/>
- <li class="gl-m-0! gl-p-0!">
- <p class="form-text text-muted">
- <gl-sprintf :message="commitTemplateHintText">
- <template #link="{ content }">
- <gl-link :href="commitTemplateHelpPage" class="inline-link" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </li>
- </ul>
- </div>
+ <merge-failed-pipeline-confirmation-dialog
+ :visible="isPipelineFailedModalVisibleNormalMerge"
+ @mergeWithFailedPipeline="onMergeWithFailedPipelineConfirmation"
+ @cancel="isPipelineFailedModalVisibleNormalMerge = false"
+ />
+ </gl-button-group>
+ <merge-train-helper-icon v-if="shouldRenderMergeTrainHelperIcon" class="gl-mx-3" />
+ </template>
<div
- v-if="!shouldShowMergeControls"
+ v-else
class="gl-w-full gl-order-n1 mr-widget-merge-details"
data-qa-selector="merged_status_content"
>
@@ -661,13 +686,11 @@ export default {
{{ __('Merge details') }}
</p>
<ul class="gl-pl-4 gl-mb-0 gl-ml-3 gl-text-gray-600">
- <li v-if="mr.divergedCommitsCount > 0" class="gl-line-height-normal">
- <gl-sprintf
- :message="s__('mrWidget|The source branch is %{link} the target branch')"
- >
+ <li v-if="sourceHasDivergedFromTarget" class="gl-line-height-normal">
+ <gl-sprintf :message="$options.i18n.sourceDivergedFromTargetText">
<template #link>
<gl-link :href="mr.targetBranchPath">{{
- n__('%d commit behind', '%d commits behind', mr.divergedCommitsCount)
+ $options.i18n.divergedCommits(mr.divergedCommitsCount)
}}</gl-link>
</template>
</gl-sprintf>
@@ -696,29 +719,6 @@ export default {
</li>
</ul>
</div>
- <div
- v-else
- :class="{ 'gl-mb-5': shouldShowMergeControls }"
- class="gl-w-full gl-order-n1 gl-text-gray-500"
- >
- <added-commit-message
- :is-squash-enabled="squashBeforeMerge"
- :is-fast-forward-enabled="!shouldShowMergeEdit"
- :commits-count="commitsCount"
- :target-branch="stateData.targetBranch"
- />
- <template v-if="mr.relatedLinks">
- &middot;
- <related-links
- :state="mr.state"
- :related-links="mr.relatedLinks"
- :show-assign-to-me="false"
- :diverged-commits-count="mr.divergedCommitsCount"
- :target-branch-path="mr.targetBranchPath"
- class="mr-ready-merge-related-links gl-display-inline"
- />
- </template>
- </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 0458e9dfaf5..dee27a5d5b5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -2,7 +2,7 @@
import { GlButton } from '@gitlab/ui';
import { produce } from 'immer';
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import MergeRequest from '~/merge_request';
@@ -77,7 +77,7 @@ export default {
},
) {
if (errors?.length) {
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
@@ -130,7 +130,7 @@ export default {
},
)
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
}),
)
@@ -152,7 +152,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
deleted file mode 100644
index 18fdb29ba54..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
+++ /dev/null
@@ -1,137 +0,0 @@
-<script>
-import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import Poll from '~/lib/utils/poll';
-import { n__ } from '~/locale';
-import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
-import TerraformPlan from './terraform_plan.vue';
-
-export default {
- name: 'MRWidgetTerraformContainer',
- components: {
- GlSkeletonLoader,
- GlSprintf,
- MrWidgetExpanableSection,
- TerraformPlan,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- loading: true,
- plansObject: {},
- poll: null,
- };
- },
- computed: {
- inValidPlanCountText() {
- if (this.numberOfInvalidPlans === 0) {
- return null;
- }
-
- return n__(
- 'Terraform|%{number} Terraform report failed to generate',
- 'Terraform|%{number} Terraform reports failed to generate',
- this.numberOfInvalidPlans,
- );
- },
- numberOfInvalidPlans() {
- return Object.values(this.plansObject).filter((plan) => plan.tf_report_error).length;
- },
- numberOfPlans() {
- return Object.keys(this.plansObject).length;
- },
- numberOfValidPlans() {
- return this.numberOfPlans - this.numberOfInvalidPlans;
- },
- validPlanCountText() {
- if (this.numberOfValidPlans === 0) {
- return null;
- }
-
- return n__(
- 'Terraform|%{number} Terraform report was generated in your pipelines',
- 'Terraform|%{number} Terraform reports were generated in your pipelines',
- this.numberOfValidPlans,
- );
- },
- },
- created() {
- this.fetchPlans();
- },
- beforeDestroy() {
- this.poll.stop();
- },
- methods: {
- fetchPlans() {
- this.loading = true;
-
- this.poll = new Poll({
- resource: {
- fetchPlans: () => axios.get(this.endpoint),
- },
- data: this.endpoint,
- method: 'fetchPlans',
- successCallback: ({ data }) => {
- this.plansObject = data;
-
- if (this.numberOfPlans > 0) {
- this.loading = false;
- this.poll.stop();
- }
- },
- errorCallback: () => {
- this.plansObject = { bad_plan: { tf_report_error: 'api_error' } };
- this.loading = false;
- this.poll.stop();
- },
- });
-
- this.poll.makeRequest();
- },
- },
-};
-</script>
-
-<template>
- <section class="mr-widget-section">
- <div v-if="loading" class="mr-widget-body">
- <gl-skeleton-loader />
- </div>
-
- <mr-widget-expanable-section v-else>
- <template #header>
- <div
- data-testid="terraform-header-text"
- class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column"
- >
- <p v-if="validPlanCountText" class="gl-m-0">
- <gl-sprintf :message="validPlanCountText">
- <template #number>
- <strong>{{ numberOfValidPlans }}</strong>
- </template>
- </gl-sprintf>
- </p>
-
- <p v-if="inValidPlanCountText" class="gl-m-0">
- <gl-sprintf :message="inValidPlanCountText">
- <template #number>
- <strong>{{ numberOfInvalidPlans }}</strong>
- </template>
- </gl-sprintf>
- </p>
- </div>
- </template>
-
- <template #content>
- <div class="mr-widget-body gl-pb-1">
- <terraform-plan v-for="(plan, key) in plansObject" :key="key" :plan="plan" />
- </div>
- </template>
- </mr-widget-expanable-section>
- </section>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
deleted file mode 100644
index 1e5f7361966..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
+++ /dev/null
@@ -1,119 +0,0 @@
-<script>
-import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
-import { s__ } from '~/locale';
-
-export default {
- name: 'TerraformPlan',
- components: {
- GlIcon,
- GlLink,
- GlSprintf,
- },
- props: {
- plan: {
- required: true,
- type: Object,
- },
- },
- i18n: {
- changes: s__(
- 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
- ),
- generationErrored: s__('Terraform|Generating the report caused an error.'),
- namedReportFailed: s__('Terraform|The job %{name} failed to generate a report.'),
- namedReportGenerated: s__('Terraform|The job %{name} generated a report.'),
- reportFailed: s__('Terraform|A report failed to generate.'),
- reportGenerated: s__('Terraform|A report was generated in your pipelines.'),
- },
- computed: {
- addNum() {
- return Number(this.plan.create);
- },
- changeNum() {
- return Number(this.plan.update);
- },
- deleteNum() {
- return Number(this.plan.delete);
- },
- iconType() {
- return this.validPlanValues ? 'doc-changes' : 'warning';
- },
- reportChangeText() {
- if (this.validPlanValues) {
- return this.$options.i18n.changes;
- }
-
- return this.$options.i18n.generationErrored;
- },
- reportHeaderText() {
- if (this.validPlanValues) {
- return this.plan.job_name
- ? this.$options.i18n.namedReportGenerated
- : this.$options.i18n.reportGenerated;
- }
-
- return this.plan.job_name
- ? this.$options.i18n.namedReportFailed
- : this.$options.i18n.reportFailed;
- },
- validPlanValues() {
- return this.addNum + this.changeNum + this.deleteNum >= 0;
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-display-flex gl-pb-3">
- <span
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-align-self-start gl-px-2"
- >
- <gl-icon :name="iconType" :size="16" data-testid="change-type-icon" />
- </span>
-
- <div class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column flex-md-row gl-pl-3">
- <div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-pr-3">
- <p class="gl-mb-3 gl-line-height-normal">
- <gl-sprintf :message="reportHeaderText">
- <template #name>
- <strong>{{ plan.job_name }}</strong>
- </template>
- </gl-sprintf>
- </p>
-
- <p class="gl-mb-3 gl-line-height-normal">
- <gl-sprintf :message="reportChangeText">
- <template #addNum>
- <strong>{{ addNum }}</strong>
- </template>
-
- <template #changeNum>
- <strong>{{ changeNum }}</strong>
- </template>
-
- <template #deleteNum>
- <strong>{{ deleteNum }}</strong>
- </template>
- </gl-sprintf>
- </p>
- </div>
-
- <div>
- <gl-link
- v-if="plan.job_path"
- :href="plan.job_path"
- target="_blank"
- data-testid="terraform-report-link"
- data-track-action="click_terraform_mr_plan_button"
- data-track-label="mr_widget_terraform_mr_plan_button"
- data-track-property="terraform_mr_plan_button"
- class="btn btn-sm"
- rel="noopener"
- >
- {{ __('View full log') }}
- <gl-icon name="external-link" />
- </gl-link>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
new file mode 100644
index 00000000000..d1ade2886f4
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlBadge, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
+import Actions from '../action_buttons.vue';
+import { generateText } from '../extensions/utils';
+import ContentRow from './widget_content_row.vue';
+
+export default {
+ name: 'DynamicContent',
+ components: {
+ GlBadge,
+ GlLink,
+ Actions,
+ ContentRow,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ widgetName: {
+ type: String,
+ required: true,
+ },
+ level: {
+ type: Number,
+ required: false,
+ default: 2,
+ },
+ },
+ computed: {
+ statusIcon() {
+ return this.data.icon?.name || undefined;
+ },
+ generatedText() {
+ return generateText(this.data.text);
+ },
+ generatedSubtext() {
+ return generateText(this.data.subtext);
+ },
+ generatedSupportingText() {
+ return generateText(this.data.supportingText);
+ },
+ },
+ methods: {
+ onClickedAction(action) {
+ this.$emit('clickedAction', action);
+ },
+ },
+};
+</script>
+
+<template>
+ <content-row
+ :level="level"
+ :status-icon-name="statusIcon"
+ :widget-name="widgetName"
+ :header="data.header"
+ >
+ <template #body>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div>
+ <p v-safe-html="generatedText" class="gl-mb-0"></p>
+ <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link>
+ <p v-if="data.supportingText" v-safe-html="generatedSupportingText" class="gl-mb-0"></p>
+ <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
+ {{ data.badge.text }}
+ </gl-badge>
+ <actions
+ :widget="widgetName"
+ :tertiary-buttons="data.actions"
+ class="gl-ml-auto gl-pl-3"
+ @clickedAction="onClickedAction"
+ />
+ <p v-if="data.subtext" v-safe-html="generatedSubtext" class="gl-m-0 gl-font-sm"></p>
+ </div>
+ <ul
+ v-if="data.children && data.children.length > 0 && level === 2"
+ class="gl-m-0 gl-p-0 gl-list-style-none"
+ >
+ <li>
+ <dynamic-content
+ v-for="(childData, index) in data.children"
+ :key="childData.id || index"
+ :data="childData"
+ :widget-name="widgetName"
+ :level="3"
+ data-qa-selector="child_content"
+ @clickedAction="onClickedAction"
+ />
+ </li>
+ </ul>
+ </div>
+ </template>
+ </content-row>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
new file mode 100644
index 00000000000..ff17de343d6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { EXTENSION_ICON_CLASS, EXTENSION_ICON_NAMES } from '../../constants';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlIcon,
+ },
+ props: {
+ level: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ iconName: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ iconAriaLabel() {
+ return `${capitalizeFirstCharacter(this.iconName)} ${this.name}`;
+ },
+ iconSize() {
+ return this.level === 1 ? 16 : 12;
+ },
+ },
+ EXTENSION_ICON_NAMES,
+ EXTENSION_ICON_CLASS,
+};
+</script>
+
+<template>
+ <div :class="[$options.EXTENSION_ICON_CLASS[iconName]]" class="gl-mr-3">
+ <gl-loading-icon v-if="isLoading" size="md" inline />
+ <div
+ v-else
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-rounded-full gl-bg-gray-10"
+ :class="{
+ 'gl-p-2': level === 1,
+ }"
+ >
+ <div class="gl-rounded-full gl-bg-white">
+ <gl-icon
+ :name="$options.EXTENSION_ICON_NAMES[iconName]"
+ :size="iconSize"
+ :aria-label="iconAriaLabel"
+ :data-qa-selector="`status_${iconName}_icon`"
+ class="gl-display-block"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
index c9fc2dde0bd..94359d7d6ac 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
@@ -4,10 +4,11 @@ import * as Sentry from '@sentry/browser';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { sprintf, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
-import StatusIcon from '../extensions/status_icon.vue';
import ActionButtons from '../action_buttons.vue';
import { EXTENSION_ICONS } from '../../constants';
-import ContentSection from './widget_content_section.vue';
+import ContentRow from './widget_content_row.vue';
+import DynamicContent from './dynamic_content.vue';
+import StatusIcon from './status_icon.vue';
const FETCH_TYPE_COLLAPSED = 'collapsed';
const FETCH_TYPE_EXPANDED = 'expanded';
@@ -18,7 +19,8 @@ export default {
StatusIcon,
GlButton,
GlLoadingIcon,
- ContentSection,
+ ContentRow,
+ DynamicContent,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,7 +61,7 @@ export default {
},
// If the content slot is not used, this value will be used as a fallback.
content: {
- type: Object,
+ type: Array,
required: false,
default: undefined,
},
@@ -187,7 +189,7 @@ export default {
<template>
<section class="media-section" data-testid="widget-extension">
- <div class="media gl-p-5">
+ <div class="gl-p-5 gl-align-items-center gl-display-flex">
<status-icon
:level="1"
:name="widgetName"
@@ -227,23 +229,34 @@ export default {
</div>
<div
v-if="!isCollapsed || contentError"
- class="mr-widget-grouped-section gl-relative"
+ class="gl-relative gl-bg-gray-10"
data-testid="widget-extension-collapsed-section"
>
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
- <gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
+ <gl-loading-icon size="sm" inline /> {{ loadingText }}
+ </div>
+ <div v-else class="gl-px-5 gl-display-flex">
+ <content-row
+ v-if="contentError"
+ :level="2"
+ :status-icon-name="$options.failedStatusIcon"
+ :widget-name="widgetName"
+ >
+ <template #body>
+ {{ contentError }}
+ </template>
+ </content-row>
+ <div v-else class="gl-w-full">
+ <slot name="content">
+ <dynamic-content
+ v-for="(data, index) in content"
+ :key="data.id || index"
+ :data="data"
+ :widget-name="widgetName"
+ />
+ </slot>
+ </div>
</div>
- <content-section
- v-else-if="contentError"
- class="report-block-container"
- :status-icon-name="$options.failedStatusIcon"
- :widget-name="widgetName"
- >
- {{ contentError }}
- </content-section>
- <slot v-else name="content">
- {{ content }}
- </slot>
</div>
</section>
</template>
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
new file mode 100644
index 00000000000..ee81f0950a8
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
@@ -0,0 +1,68 @@
+<script>
+import { GlSafeHtmlDirective } from '@gitlab/ui';
+import { EXTENSION_ICONS } from '../../constants';
+import { generateText } from '../extensions/utils';
+import StatusIcon from './status_icon.vue';
+
+export default {
+ components: {
+ StatusIcon,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ props: {
+ level: {
+ type: Number,
+ required: true,
+ validator: (value) => value === 2 || value === 3,
+ },
+ statusIconName: {
+ type: String,
+ default: '',
+ required: false,
+ validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value),
+ },
+ widgetName: {
+ type: String,
+ required: true,
+ },
+ header: {
+ type: [String, Array],
+ default: '',
+ required: false,
+ },
+ },
+ computed: {
+ generatedHeader() {
+ return generateText(Array.isArray(this.header) ? this.header[0] : this.header);
+ },
+ generatedSubheader() {
+ return Array.isArray(this.header) && this.header[1] ? generateText(this.header[1]) : '';
+ },
+ },
+};
+</script>
+<template>
+ <div
+ class="gl-w-full gl-display-flex mr-widget-content-row gl-align-items-baseline"
+ :class="{ 'gl-border-t gl-py-3 gl-pl-7': level === 2 }"
+ >
+ <status-icon v-if="statusIconName" :level="2" :name="widgetName" :icon-name="statusIconName" />
+ <div>
+ <slot name="header">
+ <div v-if="header" class="gl-mb-2">
+ <strong v-safe-html="generatedHeader" class="gl-display-block"></strong
+ ><span
+ v-if="generatedSubheader"
+ v-safe-html="generatedSubheader"
+ class="gl-display-block"
+ ></span>
+ </div>
+ </slot>
+ <div class="gl-display-flex gl-align-items-baseline gl-w-full">
+ <slot name="body"></slot>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue
deleted file mode 100644
index 61e3744b5dc..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<script>
-import { EXTENSION_ICONS } from '../../constants';
-import StatusIcon from '../extensions/status_icon.vue';
-
-export default {
- components: {
- StatusIcon,
- },
- props: {
- statusIconName: {
- type: String,
- default: '',
- required: false,
- validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value),
- },
- widgetName: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-<template>
- <div class="gl-px-7">
- <div class="gl-pl-4 gl-display-flex">
- <status-icon
- v-if="statusIconName"
- :level="2"
- :name="widgetName"
- :icon-name="statusIconName"
- />
- <slot name="default"></slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index be4e34ffff0..c6baf3b46ff 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -180,3 +180,16 @@ export const INVALID_RULES_DOCS_PATH = helpPagePath(
anchor: 'invalid-rules',
},
);
+
+export const DETAILED_MERGE_STATUS = {
+ MERGEABLE: 'MERGEABLE',
+ CHECKING: 'CHECKING',
+ NOT_OPEN: 'NOT_OPEN',
+ DISCUSSIONS_NOT_RESOLVED: 'DISCUSSIONS_NOT_RESOLVED',
+ NOT_APPROVED: 'NOT_APPROVED',
+ DRAFT_STATUS: 'DRAFT_STATUS',
+ BLOCKED_STATUS: 'BLOCKED_STATUS',
+ POLICIES_DENIED: 'POLICIES_DENIED',
+ CI_MUST_PASS: 'CI_MUST_PASS',
+ CI_STILL_RUNNING: 'CI_STILL_RUNNING',
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index c8a2a8d119b..a3f70b551bf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -6,7 +6,7 @@ import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/ap
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import { stateToComponentMap as classState } from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import notify from '~/lib/utils/notify';
import { sprintf, s__, __ } from '~/locale';
@@ -86,9 +86,6 @@ export default {
import('../reports/codequality_report/grouped_codequality_reports_app.vue'),
GroupedTestReportsApp: () =>
import('../reports/grouped_test_report/grouped_test_reports_app.vue'),
- TerraformPlan: () => import('./components/terraform/mr_widget_terraform_container.vue'),
- GroupedAccessibilityReportsApp: () =>
- import('../reports/accessibility_report/grouped_accessibility_reports_app.vue'),
MrWidgetApprovals,
SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'),
MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'),
@@ -218,12 +215,6 @@ export default {
hasAlerts() {
return this.hasMergeError || this.showMergePipelineForkWarning;
},
- shouldShowExtension() {
- return (
- window.gon?.features?.refactorMrWidgetsExtensions ||
- window.gon?.features?.refactorMrWidgetsExtensionsUser
- );
- },
shouldShowSecurityExtension() {
return window.gon?.features?.refactorSecurityExtension;
},
@@ -276,7 +267,7 @@ export default {
this.initWidget(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Unable to load the merge request widget. Try reloading the page.'),
}),
);
@@ -368,7 +359,7 @@ export default {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
}),
);
@@ -427,7 +418,7 @@ export default {
.catch(() => this.throwDeploymentsError());
},
throwDeploymentsError() {
- createFlash({
+ createAlert({
message: __(
'Something went wrong while fetching the environments for this merge request. Please try again.',
),
@@ -447,7 +438,7 @@ export default {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
}),
);
@@ -506,17 +497,24 @@ export default {
eventHub.$on('DisablePolling', () => {
this.stopPolling();
});
+
+ eventHub.$on('FetchDeployments', () => {
+ this.fetchPreMergeDeployments();
+ if (this.shouldRenderMergedPipeline) {
+ this.fetchPostMergeDeployments();
+ }
+ });
},
dismissSuggestPipelines() {
this.mr.isDismissedSuggestPipeline = true;
},
registerTerraformPlans() {
- if (this.shouldRenderTerraformPlans && this.shouldShowExtension) {
+ if (this.shouldRenderTerraformPlans) {
registerExtension(terraformExtension);
}
},
registerAccessibilityExtension() {
- if (this.shouldShowAccessibilityReport && this.shouldShowExtension) {
+ if (this.shouldShowAccessibilityReport) {
registerExtension(accessibilityExtension);
}
},
@@ -620,16 +618,6 @@ export default {
:pipeline-path="mr.pipeline.path"
/>
- <terraform-plan
- v-if="mr.terraformReportsPath && !shouldShowExtension"
- :endpoint="mr.terraformReportsPath"
- />
-
- <grouped-accessibility-reports-app
- v-if="shouldShowAccessibilityReport && !shouldShowExtension"
- :endpoint="mr.accessibilityReportPath"
- />
-
<div class="mr-widget-section" data-qa-selector="mr_widget_content">
<component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
index eac72ffb2f2..516ba104d7b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
@@ -10,6 +10,7 @@ query getState($projectPath: ID!, $iid: String!) {
availableAutoMergeStrategies
commitCount
conflicts
+ detailedMergeStatus
diffHeadSha
mergeError
mergeStatus
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 7a458f9ce7e..81cb20475cc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -1,3 +1,4 @@
+import { DETAILED_MERGE_STATUS } from '../constants';
import { stateKey } from './state_maps';
export default function deviseState() {
@@ -7,7 +8,7 @@ export default function deviseState() {
return stateKey.archived;
} else if (this.branchMissing) {
return stateKey.missingBranch;
- } else if (this.mergeStatus === 'unchecked' || this.mergeStatus === 'checking') {
+ } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CHECKING) {
return stateKey.checking;
} else if (this.hasConflicts) {
return stateKey.conflicts;
@@ -15,19 +16,20 @@ export default function deviseState() {
return stateKey.rebase;
} else if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
return stateKey.mergeChecksFailed;
- } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
+ } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_MUST_PASS) {
return stateKey.pipelineFailed;
- } else if (this.draft) {
+ } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DRAFT_STATUS) {
return stateKey.draft;
- } else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
+ } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DISCUSSIONS_NOT_RESOLVED) {
return stateKey.unresolvedDiscussions;
- } else if (this.isPipelineBlocked) {
- return stateKey.pipelineBlocked;
} else if (this.canMerge && this.isSHAMismatch) {
return stateKey.shaMismatch;
} else if (this.autoMergeEnabled && !this.mergeError) {
return stateKey.autoMergeEnabled;
- } else if (this.canBeMerged) {
+ } else if (
+ this.detailedMergeStatus === DETAILED_MERGE_STATUS.MERGEABLE ||
+ this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_STILL_RUNNING
+ ) {
return stateKey.readyToMerge;
}
return null;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index e6ff586892f..731d3886f61 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -219,6 +219,7 @@ export default class MergeRequestStore {
this.shouldBeRebased = mergeRequest.shouldBeRebased;
this.draft = mergeRequest.draft;
this.mergeRequestState = mergeRequest.state;
+ this.detailedMergeStatus = mergeRequest.detailedMergeStatus;
this.setState();
}
@@ -291,6 +292,7 @@ export default class MergeRequestStore {
this.suggestPipelineFeatureId = data.suggest_pipeline_feature_id;
this.isDismissedSuggestPipeline = data.is_dismissed_suggest_pipeline;
this.securityReportsDocsPath = data.security_reports_docs_path;
+ this.securityConfigurationPath = data.security_configuration_path;
// code quality
const blobPath = data.blob_path || {};
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 84bd6bca601..c93057c491c 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,6 +1,5 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import { visitUrl } from '~/lib/utils/url_utility';
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import CiIcon from './ci_icon.vue';
/**
* Renders CI Badge link with CI icon and status text based on
@@ -27,6 +26,7 @@ import CiIcon from './ci_icon.vue';
export default {
components: {
+ GlLink,
CiIcon,
},
directives: {
@@ -61,29 +61,21 @@ export default {
return className ? `ci-status ci-${className}` : 'ci-status';
},
},
- methods: {
- navigateToPipeline() {
- visitUrl(this.detailsPath);
-
- // event used for tracking
- this.$emit('ciStatusBadgeClick');
- },
- },
};
</script>
<template>
- <a
+ <gl-link
v-gl-tooltip
:class="cssClass"
- class="gl-cursor-pointer"
:title="title"
data-qa-selector="status_badge_link"
- @click="navigateToPipeline"
+ :href="detailsPath"
+ @click="$emit('ciStatusBadgeClick')"
>
<ci-icon :status="status" :css-classes="iconClasses" />
<template v-if="showText">
{{ status.text }}
</template>
- </a>
+ </gl-link>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
index a88a4ca5cb8..75386a3cd01 100644
--- a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
@@ -1,6 +1,6 @@
<script>
import { isString } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { DEFAULT_COLOR, COLOR_WIDGET_COLOR, DROPDOWN_VARIANT, ISSUABLE_COLORS } from './constants';
@@ -97,7 +97,7 @@ export default {
return DEFAULT_COLOR;
},
error() {
- createFlash({
+ createAlert({
message: this.$options.i18n.fetchingError,
captureError: true,
});
@@ -161,7 +161,7 @@ export default {
});
})
.catch((error) =>
- createFlash({
+ createAlert({
message: this.$options.i18n.updatingError,
captureError: true,
error,
diff --git a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
index 298c7bc50cc..31c98d1e3a7 100644
--- a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
+++ b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
@@ -33,7 +33,7 @@ export default {
:title="confidentialTooltip"
icon="eye-slash"
variant="warning"
- class="gl-display-inline gl-mr-2"
+ class="gl-display-inline gl-mr-3"
>{{ __('Confidential') }}</gl-badge
>
</template>
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 ffe09634a3b..4873996d357 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
@@ -56,3 +56,9 @@ export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization');
export const TOKEN_TITLE_RELEASE = __('Release');
export const TOKEN_TITLE_TYPE = __('Type');
+
+// 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
+// from the CE component, we need to keep this in the CE code.
+// https://gitlab.com/gitlab-org/gitlab/-/issues/377838
+export const TOKEN_TYPE_HEALTH = 'health_status';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index e311df6e66f..8821084ef35 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -12,7 +12,7 @@ import {
import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { SortDirection } from './constants';
@@ -197,7 +197,7 @@ export default {
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
- createFlash({
+ createAlert({
message: __('An error occurred while parsing recent searches'),
});
@@ -346,6 +346,11 @@ export default {
:suggestions-list-class="suggestionsListClass"
:search-button-attributes="searchButtonAttributes"
:search-input-attributes="searchInputAttributes"
+ :recent-searches-header="__('Recent searches')"
+ :clear-button-title="__('Clear')"
+ :close-button-title="__('Close')"
+ :clear-recent-searches-text="__('Clear recent searches')"
+ :no-recent-searches-text="__(`You don't have any recent searches`)"
class="flex-grow-1"
@history-item-selected="handleHistoryItemSelected"
@clear="onClear"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
index 7c4e372dda1..8a6053b7001 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -24,7 +24,7 @@ export function fetchBranches({ commit, state }, search = '') {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_BRANCHES_ERROR, status);
- createFlash({
+ createAlert({
message: __('Failed to load branches. Please try again.'),
});
});
@@ -43,7 +43,7 @@ export const fetchMilestones = ({ commit, state }, searchTitle = '') => {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_MILESTONES_ERROR, status);
- createFlash({
+ createAlert({
message: __('Failed to load milestones. Please try again.'),
});
});
@@ -61,7 +61,7 @@ export const fetchLabels = ({ commit, state }, search = '') => {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_LABELS_ERROR, status);
- createFlash({
+ createAlert({
message: __('Failed to load labels. Please try again.'),
});
});
@@ -86,7 +86,7 @@ function fetchUser(options = {}) {
.catch(({ response }) => {
const { status } = response;
commit(`RECEIVE_${action}_ERROR`, status);
- createFlash({
+ createAlert({
message: errorMessage,
});
});
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index 848c49c48c7..7c184a3c391 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -1,7 +1,7 @@
<script>
import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { compact } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { DEFAULT_NONE_ANY } from '../constants';
@@ -65,7 +65,7 @@ export default {
this.authors = Array.isArray(res) ? compact(res) : compact(res.data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was a problem fetching users.'),
}),
)
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
index aa5161ca93c..741395b3193 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -46,7 +46,7 @@ export default {
this.branches = data;
})
.catch(() => {
- createFlash({ message: __('There was a problem fetching branches.') });
+ createAlert({ message: __('There was a problem fetching branches.') });
})
.finally(() => {
this.loading = false;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
index adfe0559b62..d34cfb922a9 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
@@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { ITEM_TYPE } from '~/groups/constants';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import searchCrmContactsQuery from '../queries/search_crm_contacts.query.graphql';
@@ -81,7 +81,7 @@ export default {
: data[this.namespace]?.contacts.nodes;
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was a problem fetching CRM contacts.'),
}),
)
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
index e6ab944449e..c7c9350ee93 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
@@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { ITEM_TYPE } from '~/groups/constants';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import searchCrmOrganizationsQuery from '../queries/search_crm_organizations.query.graphql';
@@ -78,7 +78,7 @@ export default {
: data[this.namespace]?.organizations.nodes;
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was a problem fetching CRM organizations.'),
}),
)
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
index 210d814d22a..929823f7308 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { DEFAULT_NONE_ANY } from '../constants';
@@ -48,7 +48,7 @@ export default {
this.emojis = Array.isArray(response) ? response : response.data;
})
.catch(() => {
- createFlash({ message: __('There was a problem fetching emojis.') });
+ createAlert({ message: __('There was a problem fetching emojis.') });
})
.finally(() => {
this.loading = false;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 178c57a5666..bce0c11aafd 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -1,7 +1,7 @@
<script>
import { GlToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -81,7 +81,7 @@ export default {
this.labels = Array.isArray(res) ? res : res.data;
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was a problem fetching labels.'),
}),
)
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 69265d0fdc9..b9ee4d51db1 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { sortMilestonesByDueDate } from '~/milestones/utils';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -65,7 +65,7 @@ export default {
}
})
.catch(() => {
- createFlash({ message: __('There was a problem fetching milestones.') });
+ createAlert({ message: __('There was a problem fetching milestones.') });
})
.finally(() => {
this.loading = false;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
index 9e68c92af5d..59701b4959e 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { DEFAULT_NONE_ANY } from '../constants';
@@ -47,7 +47,7 @@ export default {
this.releases = response;
})
.catch(() => {
- createFlash({ message: __('There was a problem fetching releases.') });
+ createAlert({ message: __('There was a problem fetching releases.') });
})
.finally(() => {
this.loading = false;
diff --git a/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue b/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
index 72148a0aa7c..c2be5e4f7a1 100644
--- a/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
+++ b/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
@@ -1,8 +1,10 @@
<script>
import { GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
+import Tracking from '~/tracking';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
+import { helpPagePath } from '~/helpers/help_page_helper';
const STATUS_TYPES = {
SUCCESS: 'success',
@@ -10,11 +12,14 @@ const STATUS_TYPES = {
DANGER: 'danger',
};
+const UPGRADE_DOCS_URL = helpPagePath('update/index');
+
export default {
name: 'GitlabVersionCheck',
components: {
GlBadge,
},
+ mixins: [Tracking.mixin()],
props: {
size: {
type: String,
@@ -50,6 +55,10 @@ export default {
.then((res) => {
if (res.data) {
this.status = res.data.severity;
+
+ this.track('rendered_version_badge', {
+ label: this.title,
+ });
}
})
.catch(() => {
@@ -57,12 +66,24 @@ export default {
this.status = null;
});
},
+ onClick() {
+ this.track('click_version_badge', { label: this.title });
+ },
},
+ UPGRADE_DOCS_URL,
};
</script>
<template>
- <gl-badge v-if="status" class="version-check-badge" :variant="status" :size="size">{{
- title
- }}</gl-badge>
+ <!-- TODO: remove the span element once bootstrap-vue is updated to version 2.21.1 -->
+ <!-- TODO: https://github.com/bootstrap-vue/bootstrap-vue/issues/6219 -->
+ <span v-if="status" data-testid="badge-click-wrapper" @click="onClick">
+ <gl-badge
+ :href="$options.UPGRADE_DOCS_URL"
+ class="version-check-badge"
+ :variant="status"
+ :size="size"
+ >{{ title }}</gl-badge
+ >
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/utils.js b/app/assets/javascripts/vue_shared/components/group_select/utils.js
new file mode 100644
index 00000000000..0a4622269f4
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/group_select/utils.js
@@ -0,0 +1,15 @@
+import Api from '~/api';
+
+export const groupsPath = (groupsFilter, parentGroupID) => {
+ if (groupsFilter !== undefined && parentGroupID === undefined) {
+ throw new Error('Cannot use groupsFilter without a parentGroupID');
+ }
+ switch (groupsFilter) {
+ case 'descendant_groups':
+ return Api.descendantGroupsPath.replace(':id', parentGroupID);
+ case 'subgroups':
+ return Api.subgroupsPath.replace(':id', parentGroupID);
+ default:
+ return Api.groupsPath;
+ }
+};
diff --git a/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js
new file mode 100644
index 00000000000..d80c1ff8b0c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js
@@ -0,0 +1,12 @@
+import { issuableTypes } from '~/boards/constants';
+import blockingIssuesQuery from './graphql/blocking_issues.query.graphql';
+import blockingEpicsQuery from './graphql/blocking_epics.query.graphql';
+
+export const blockingIssuablesQueries = {
+ [issuableTypes.issue]: {
+ query: blockingIssuesQuery,
+ },
+ [issuableTypes.epic]: {
+ query: blockingEpicsQuery,
+ },
+};
diff --git a/app/assets/javascripts/boards/graphql/board_blocking_epics.query.graphql b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_epics.query.graphql
index 071a6d7410f..4b9a9243052 100644
--- a/app/assets/javascripts/boards/graphql/board_blocking_epics.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_epics.query.graphql
@@ -1,4 +1,4 @@
-query BoardBlockingEpics($fullPath: ID!, $iid: ID) {
+query BlockingEpics($fullPath: ID!, $iid: ID) {
group(fullPath: $fullPath) {
id
issuable: epic(iid: $iid) {
diff --git a/app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_issues.query.graphql
index 01fab571733..279c2202740 100644
--- a/app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_issues.query.graphql
@@ -1,4 +1,4 @@
-query BoardBlockingIssues($id: IssueID!) {
+query BlockingIssues($id: IssueID!) {
issuable: issue(id: $id) {
id
blockingIssuables: blockedByIssues {
diff --git a/app/assets/javascripts/boards/components/board_blocked_icon.vue b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue
index 3f8a596abd8..253aca8837d 100644
--- a/app/assets/javascripts/boards/components/board_blocked_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue
@@ -1,10 +1,11 @@
<script>
import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
-import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants';
+import { issuableTypes } from '~/boards/constants';
import { TYPE_ISSUE, TYPE_EPIC } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { truncate } from '~/lib/utils/text_utility';
import { __, n__, s__, sprintf } from '~/locale';
+import { blockingIssuablesQueries } from './constants';
export default {
i18n: {
@@ -28,7 +29,6 @@ export default {
GlLink,
GlLoadingIcon,
},
- blockingIssuablesQueries,
props: {
item: {
type: Object,
@@ -169,8 +169,8 @@ export default {
:id="glIconId"
ref="icon"
:name="blockIcon"
- class="issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500"
- data-testid="issue-blocked-icon"
+ class="issuable-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500"
+ data-testid="issuable-blocked-icon"
@mouseenter="handleMouseEnter"
/>
<gl-popover :target="glIconId" placement="top">
@@ -182,12 +182,19 @@ export default {
<p class="gl-mt-4 gl-mb-0 gl-font-small">{{ loadingMessage }}</p>
</template>
<template v-else>
- <ul class="gl-list-style-none gl-p-0">
- <li v-for="issuable in displayedIssuables" :key="issuable.id">
+ <ul class="gl-list-style-none gl-p-0 gl-mb-0">
+ <li v-for="(issuable, index) in displayedIssuables" :key="issuable.id">
<gl-link :href="issuable.webUrl" class="gl-text-blue-500! gl-font-sm">{{
issuable.reference
}}</gl-link>
- <p class="gl-mb-3 gl-display-block!" data-testid="issuable-title">
+ <p
+ class="gl-display-block!"
+ :class="{
+ 'gl-mb-3': index < displayedIssuables.length - 1,
+ 'gl-mb-0': index === displayedIssuables.length - 1,
+ }"
+ data-testid="issuable-title"
+ >
{{ issuable.title }}
</p>
</li>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index 926034efd10..caec49c557a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -51,6 +51,7 @@ export default {
<gl-dropdown
:text="dropdownText"
:disabled="disabled"
+ size="small"
boundary="window"
right
lazy
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 32b3a0e22c2..657e4498b53 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -3,7 +3,7 @@ import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { debounce, unescape } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import GLForm from '~/gl_form';
import axios from '~/lib/utils/axios_utils';
import { stripHtml } from '~/lib/utils/text_utility';
@@ -272,7 +272,7 @@ export default {
this.fetchMarkdown()
.then((data) => this.renderMarkdown(data))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error loading markdown preview'),
}),
);
@@ -315,7 +315,7 @@ export default {
this.$nextTick()
.then(() => $(this.$refs['markdown-preview']).renderGFM())
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error rendering Markdown preview'),
}),
);
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 458dfe0ed23..89fffdedbfd 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -7,6 +7,8 @@ import {
ITALIC_TEXT,
STRIKETHROUGH_TEXT,
LINK_TEXT,
+ INDENT_LINE,
+ OUTDENT_LINE,
} from '~/behaviors/shortcuts/keybindings';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
@@ -68,12 +70,15 @@ export default {
},
computed: {
mdTable() {
+ const header = s__('MarkdownEditor|header');
+ const divider = '-'.repeat(header.length);
+ const cell = ' '.repeat(header.length);
+
return [
- // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
- '| header | header |', // eslint-disable-line @gitlab/require-i18n-strings
- '| ------ | ------ |',
- '| cell | cell |', // eslint-disable-line @gitlab/require-i18n-strings
- '| cell | cell |', // eslint-disable-line @gitlab/require-i18n-strings
+ `| ${header} | ${header} |`,
+ `| ${divider} | ${divider} |`,
+ `| ${cell} | ${cell} |`,
+ `| ${cell} | ${cell} |`,
].join('\n');
},
mdSuggestion() {
@@ -82,7 +87,8 @@ export default {
);
},
mdCollapsibleSection() {
- return ['<details><summary>Click to expand</summary>', `{text}`, '</details>'].join('\n');
+ const expandText = s__('MarkdownEditor|Click to expand');
+ return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n');
},
isMac() {
// Accessing properties using ?. to allow tests to use
@@ -170,6 +176,8 @@ export default {
italic: keysFor(ITALIC_TEXT),
strikethrough: keysFor(STRIKETHROUGH_TEXT),
link: keysFor(LINK_TEXT),
+ indent: keysFor(INDENT_LINE),
+ outdent: keysFor(OUTDENT_LINE),
},
i18n: {
writeTabTitle: __('Write'),
@@ -235,6 +243,7 @@ export default {
variant="confirm"
category="primary"
size="small"
+ data-qa-selector="dismiss_suggestion_popover_button"
@click="handleSuggestDismissed"
>
{{ __('Got it') }}
@@ -318,6 +327,32 @@ export default {
icon="list-task"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('indent')"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.indent"
+ command="indentLines"
+ icon="list-indent"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('outdent')"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.outdent"
+ command="outdentLines"
+ icon="list-outdent"
+ />
+ <toolbar-button
v-if="!restrictedToolBarItems.includes('collapsible-section')"
:tag="mdCollapsibleSection"
:prepend="true"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
new file mode 100644
index 00000000000..b38772d5aa5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -0,0 +1,216 @@
+<script>
+import { GlSegmentedControl } from '@gitlab/ui';
+import { __ } from '~/locale';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import axios from '~/lib/utils/axios_utils';
+import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '../../constants';
+import MarkdownField from './field.vue';
+
+export default {
+ components: {
+ MarkdownField,
+ LocalStorageSync,
+ GlSegmentedControl,
+ ContentEditor: () =>
+ import(
+ /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
+ ),
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
+ },
+ renderMarkdownPath: {
+ type: String,
+ required: true,
+ },
+ markdownDocsPath: {
+ type: String,
+ required: true,
+ },
+ quickActionsDocsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ uploadsPath: {
+ type: String,
+ required: false,
+ default: () => window.uploads_path,
+ },
+ enableContentEditor: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ formFieldId: {
+ type: String,
+ required: true,
+ },
+ formFieldName: {
+ type: String,
+ required: true,
+ },
+ enablePreview: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ formFieldPlaceholder: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ formFieldAriaLabel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ initOnAutofocus: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ supportsQuickActions: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ editingMode: EDITING_MODE_MARKDOWN_FIELD,
+ switchEditingControlEnabled: true,
+ autofocus: this.initOnAutofocus,
+ };
+ },
+ computed: {
+ isContentEditorActive() {
+ return this.enableContentEditor && this.editingMode === EDITING_MODE_CONTENT_EDITOR;
+ },
+ contentEditorAutofocus() {
+ // Match textarea focus behavior
+ return this.autofocus ? 'end' : false;
+ },
+ },
+ mounted() {
+ this.autofocusTextarea(this.editingMode);
+ },
+ methods: {
+ updateMarkdownFromContentEditor({ markdown }) {
+ this.$emit('input', markdown);
+ },
+ updateMarkdownFromMarkdownField({ target }) {
+ this.$emit('input', target.value);
+ },
+ enableSwitchEditingControl() {
+ this.switchEditingControlEnabled = true;
+ },
+ disableSwitchEditingControl() {
+ this.switchEditingControlEnabled = false;
+ },
+ renderMarkdown(markdown) {
+ return axios.post(this.renderMarkdownPath, { text: markdown }).then(({ data }) => data.body);
+ },
+ onEditingModeChange(editingMode) {
+ this.notifyEditingModeChange(editingMode);
+ this.enableAutofocus(editingMode);
+ },
+ onEditingModeRestored(editingMode) {
+ this.notifyEditingModeChange(editingMode);
+ },
+ notifyEditingModeChange(editingMode) {
+ this.$emit(editingMode);
+ },
+ enableAutofocus(editingMode) {
+ this.autofocus = true;
+ this.autofocusTextarea(editingMode);
+ },
+ autofocusTextarea(editingMode) {
+ if (this.autofocus && editingMode === EDITING_MODE_MARKDOWN_FIELD) {
+ this.$refs.textarea.focus();
+ }
+ },
+ },
+ switchEditingControlOptions: [
+ { text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD },
+ { text: __('Rich text'), value: EDITING_MODE_CONTENT_EDITOR },
+ ],
+};
+</script>
+<template>
+ <div>
+ <div class="gl-display-flex gl-justify-content-start gl-mb-3">
+ <gl-segmented-control
+ v-model="editingMode"
+ data-testid="toggle-editing-mode-button"
+ data-qa-selector="editing_mode_button"
+ class="gl-display-flex"
+ :options="$options.switchEditingControlOptions"
+ :disabled="!enableContentEditor || !switchEditingControlEnabled"
+ @change="onEditingModeChange"
+ />
+ </div>
+ <local-storage-sync
+ v-model="editingMode"
+ storage-key="gl-wiki-content-editor-enabled"
+ @input="onEditingModeRestored"
+ />
+ <markdown-field
+ v-if="!isContentEditorActive"
+ :markdown-preview-path="renderMarkdownPath"
+ can-attach-file
+ :enable-autocomplete="enableAutocomplete"
+ :textarea-value="value"
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :uploads-path="uploadsPath"
+ :enable-preview="enablePreview"
+ class="bordered-box"
+ >
+ <template #textarea>
+ <textarea
+ :id="formFieldId"
+ ref="textarea"
+ :value="value"
+ :name="formFieldName"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ dir="auto"
+ :data-supports-quick-actions="supportsQuickActions"
+ data-qa-selector="markdown_editor_form_field"
+ :aria-label="formFieldAriaLabel"
+ :placeholder="formFieldPlaceholder"
+ @input="updateMarkdownFromMarkdownField"
+ @keydown="$emit('keydown', $event)"
+ >
+ </textarea>
+ </template>
+ </markdown-field>
+ <div v-else>
+ <content-editor
+ :render-markdown="renderMarkdown"
+ :uploads-path="uploadsPath"
+ :markdown="value"
+ :autofocus="contentEditorAutofocus"
+ @change="updateMarkdownFromContentEditor"
+ @loading="disableSwitchEditingControl"
+ @loadingSuccess="enableSwitchEditingControl"
+ @loadingError="enableSwitchEditingControl"
+ @keydown="$emit('keydown', $event)"
+ />
+ <input
+ :id="formFieldId"
+ :value="value"
+ :name="formFieldName"
+ data-qa-selector="markdown_editor_form_field"
+ type="hidden"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 7646a8718d6..855c7a449c4 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -139,7 +139,7 @@ export default {
</script>
<template>
- <div class="md-suggestion-header border-bottom-0 gl-mt-3">
+ <div class="md-suggestion-header border-bottom-0 gl-px-4 gl-py-3">
<div class="js-suggestion-diff-header gl-font-weight-bold">
{{ __('Suggested change') }}
<a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn">
@@ -162,6 +162,7 @@ export default {
<gl-button
class="btn-inverted js-remove-from-batch-btn btn-grouped"
:disabled="isApplying"
+ size="small"
@click="removeSuggestionFromBatch"
>
{{ __('Remove from batch') }}
@@ -172,6 +173,7 @@ export default {
class="btn-inverted js-add-to-batch-btn btn-grouped"
data-qa-selector="add_suggestion_batch_button"
:disabled="isDisableButton"
+ size="small"
@click="addSuggestionToBatch"
>
{{ __('Add suggestion to batch') }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 9b81444fc04..30d72332c90 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -1,7 +1,7 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
@@ -91,7 +91,7 @@ export default {
const suggestionElements = container.querySelectorAll('.js-render-suggestion');
if (this.lineType === 'old') {
- createFlash({
+ createAlert({
message: __('Unable to apply suggestions to a deleted line.'),
parent: this.$el,
});
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 49217e38a1b..5ca21522d33 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -47,6 +47,11 @@ export default {
required: false,
default: 0,
},
+ command: {
+ type: String,
+ required: false,
+ default: '',
+ },
/**
* A string (or an array of strings) of
@@ -81,6 +86,7 @@ export default {
:data-md-tag-content="tagContent"
:data-md-prepend="prepend"
:data-md-shortcuts="shortcutsString"
+ :data-md-command="command"
:title="buttonTitle"
:aria-label="buttonTitle"
:icon="icon"
diff --git a/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js b/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js
index 832fb891838..1c4e8d332a9 100644
--- a/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@@ -11,7 +11,7 @@ export const fetchImagesFactory = (service) => async ({ state, commit }) => {
commit(types.RECEIVE_METRIC_IMAGES_SUCCESS, response);
} catch (error) {
commit(types.RECEIVE_METRIC_IMAGES_ERROR);
- createFlash({ message: s__('MetricImages|There was an issue loading metric images.') });
+ createAlert({ message: s__('MetricImages|There was an issue loading metric images.') });
}
};
@@ -34,7 +34,7 @@ export const uploadImageFactory = (service) => async (
commit(types.RECEIVE_METRIC_UPLOAD_SUCCESS, response);
} catch (error) {
commit(types.RECEIVE_METRIC_UPLOAD_ERROR);
- createFlash({ message: s__('MetricImages|There was an issue uploading your image.') });
+ createAlert({ message: s__('MetricImages|There was an issue uploading your image.') });
}
};
@@ -57,7 +57,7 @@ export const updateImageFactory = (service) => async (
commit(types.RECEIVE_METRIC_UPDATE_SUCCESS, response);
} catch (error) {
commit(types.RECEIVE_METRIC_UPLOAD_ERROR);
- createFlash({ message: s__('MetricImages|There was an issue updating your image.') });
+ createAlert({ message: s__('MetricImages|There was an issue updating your image.') });
}
};
@@ -68,7 +68,7 @@ export const deleteImageFactory = (service) => async ({ state, commit }, imageId
await service.deleteMetricImage({ imageId, id: projectId, modelIid });
commit(types.RECEIVE_METRIC_DELETE_SUCCESS, imageId);
} catch (error) {
- createFlash({ message: s__('MetricImages|There was an issue deleting the image.') });
+ createAlert({ message: s__('MetricImages|There was an issue deleting the image.') });
}
};
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
index d4f50e347cb..41c92fdba4f 100644
--- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -61,6 +61,11 @@ export default {
required: false,
default: 'primary',
},
+ size: {
+ type: String,
+ required: false,
+ default: 'medium',
+ },
},
computed: {
modalDomId() {
@@ -103,6 +108,9 @@ export default {
:title="title"
:aria-label="title"
:category="category"
+ :size="size"
icon="copy-to-clipboard"
- />
+ >
+ <slot></slot>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue
index e9f278a5db5..ba9edc7620a 100644
--- a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue
+++ b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue
@@ -27,7 +27,7 @@ const filterByName = (data, searchTerm = '') => {
};
export default {
- name: 'NamespaceSelect',
+ name: 'NamespaceSelectDeprecated',
components: {
GlDropdown,
GlDropdownDivider,
@@ -78,7 +78,7 @@ export default {
required: false,
default: false,
},
- isLoadingMoreGroups: {
+ isLoading: {
type: Boolean,
required: false,
default: false,
@@ -152,7 +152,12 @@ export default {
};
</script>
<template>
- <gl-dropdown :text="selectedNamespaceText" :block="fullWidth" data-qa-selector="namespaces_list">
+ <gl-dropdown
+ :text="selectedNamespaceText"
+ :block="fullWidth"
+ data-qa-selector="namespaces_list"
+ @show="$emit('show')"
+ >
<template #header>
<gl-search-box-by-type
v-model.trim="searchTerm"
@@ -201,8 +206,7 @@ export default {
>{{ item.humanName }}</gl-dropdown-item
>
</div>
- <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="$emit('load-more-groups')">
- <gl-loading-icon v-if="isLoadingMoreGroups" class="gl-mb-3" size="sm" />
- </gl-intersection-observer>
+ <gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" />
+ <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="$emit('load-more-groups')" />
</gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index 0cb4a5bc39f..cf34a60c363 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -50,29 +50,19 @@ export default {
renderedNote() {
return renderMarkdown(this.note.body);
},
- avatarSize() {
- if (this.line && !this.isOverviewTab) {
- return 24;
- }
-
- return {
- default: 24,
- md: 32,
- };
- },
},
};
</script>
<template>
- <timeline-entry-item class="note note-wrapper being-posted fade-in-half">
- <div class="timeline-icon">
- <gl-avatar-link class="gl-mr-3" :href="getUserData.path">
+ <timeline-entry-item class="note note-wrapper note-comment being-posted fade-in-half">
+ <div class="timeline-avatar gl-float-left">
+ <gl-avatar-link :href="getUserData.path">
<gl-avatar
:src="getUserData.avatar_url"
:entity-name="getUserData.username"
:alt="getUserData.name"
- :size="avatarSize"
+ :size="32"
/>
</gl-avatar-link>
</div>
@@ -85,8 +75,10 @@ export default {
</a>
</div>
</div>
- <div class="note-body">
- <div v-safe-html="renderedNote" class="note-text md"></div>
+ <div class="timeline-discussion-body">
+ <div class="note-body">
+ <div v-safe-html="renderedNote" class="note-text md"></div>
+ </div>
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index 2206ae98c73..e091fe74717 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -16,7 +16,7 @@ export default {
<div class="timeline-icon"></div>
<div class="timeline-content">
<div class="note-header"></div>
- <div class="note-body"><gl-skeleton-loader /></div>
+ <div class="note-body gl-mt-4"><gl-skeleton-loader /></div>
</div>
</timeline-entry-item>
</template>
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 7e99f1b01b2..1ae5045b34f 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -129,7 +129,12 @@ export default {
<div v-safe-html:[$options.safeHtmlConfig]="iconHtml" class="timeline-icon"></div>
<div class="timeline-content">
<div class="note-header">
- <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
+ <note-header
+ :author="note.author"
+ :created-at="note.created_at"
+ :note-id="note.id"
+ :is-system-note="true"
+ >
<span ref="gfm-content" v-safe-html="actionTextHtml"></span>
<template
v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
@@ -141,7 +146,7 @@ export default {
variant="link"
:icon="descriptionVersionToggleIcon"
data-testid="compare-btn"
- class="gl-vertical-align-text-bottom"
+ class="gl-vertical-align-text-bottom gl-font-sm!"
@click="toggleDescriptionVersion"
>{{ __('Compare with previous version') }}</gl-button
>
@@ -150,7 +155,7 @@ export default {
:icon="showLines ? 'chevron-up' : 'chevron-down'"
variant="link"
data-testid="outdated-lines-change-btn"
- class="gl-vertical-align-text-bottom"
+ class="gl-vertical-align-text-bottom gl-font-sm!"
@click="toggleDiff"
>
{{ __('Compare changes') }}
@@ -190,7 +195,7 @@ export default {
</div>
<div
v-if="lines.length && showLines"
- class="diff-content gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
+ class="diff-content outdated-lines-wrapper gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
>
<table
:class="$options.userColorSchemeClass"
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
index b7768cfa5b9..df1188d365b 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
@@ -4,7 +4,7 @@ export const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
export const thClass = 'gl-hover-bg-blue-50';
export const bodyTrClass =
- 'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-cursor-pointer gl-hover-bg-blue-50 gl-hover-border-b-solid gl-hover-border-blue-200';
+ 'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-cursor-pointer gl-hover-bg-gray-50 gl-hover-border-b-solid';
export const defaultPageSize = 20;
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index 6867b5a75e3..a5027d2ca5c 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -275,7 +275,7 @@ export default {
<template>
<div class="incident-management-list">
<gl-alert v-if="showErrorMsg" variant="danger" @dismiss="$emit('error-alert-dismissed')">
- <p v-safe-html="serverErrorMessage || i18n.errorMsg"></p>
+ <span v-safe-html="serverErrorMessage || i18n.errorMsg"></span>
</gl-alert>
<div
diff --git a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
index b4d565991f5..c1246b2bf44 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
@@ -2,6 +2,7 @@
import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
const DEFAULT_PAGE_SIZES = [20, 50, 100];
@@ -12,6 +13,7 @@ export default {
GlDropdownItem,
GlIcon,
GlSprintf,
+ LocalStorageSync,
},
props: {
pageInfo: {
@@ -23,6 +25,11 @@ export default {
type: Array,
default: () => DEFAULT_PAGE_SIZES,
},
+ storageKey: {
+ required: false,
+ type: String,
+ default: null,
+ },
},
computed: {
@@ -66,6 +73,12 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-center">
+ <local-storage-sync
+ v-if="storageKey"
+ :storage-key="storageKey"
+ :value="pageInfo.perPage"
+ @input="setPageSize"
+ />
<pagination-links :change="setPage" :page-info="pageInfo" class="gl-m-0" />
<gl-dropdown category="tertiary" class="gl-ml-auto" data-testid="page-size">
<template #button-content>
diff --git a/app/assets/javascripts/vue_shared/components/registry/history_item.vue b/app/assets/javascripts/vue_shared/components/registry/history_item.vue
index a60b630b207..384b084ce09 100644
--- a/app/assets/javascripts/vue_shared/components/registry/history_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/history_item.vue
@@ -18,15 +18,15 @@ export default {
</script>
<template>
- <timeline-entry-item class="system-note note-wrapper gl-mb-6!">
+ <timeline-entry-item class="system-note note-wrapper">
<div class="timeline-icon">
<gl-icon :name="icon" />
</div>
<div class="timeline-content">
<div class="note-header">
- <span>
+ <div class="note-header-info">
<slot></slot>
- </span>
+ </div>
</div>
<div class="note-body">
<slot name="body"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
index 1948a6778f4..8c9c7c63db1 100644
--- a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
@@ -1,6 +1,7 @@
<script>
import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import { SORT_DIRECTION_UI } from '~/search/sort/constants';
const ASCENDING_ORDER = 'asc';
const DESCENDING_ORDER = 'desc';
@@ -52,6 +53,9 @@ export default {
return acc;
}, {});
},
+ sortDirectionData() {
+ return this.isSortAscending ? SORT_DIRECTION_UI.asc : SORT_DIRECTION_UI.desc;
+ },
},
methods: {
generateQueryData({ sorting = {}, filter = [] } = {}) {
@@ -119,6 +123,7 @@ export default {
data-testid="registry-sort-dropdown"
:text="sortText"
:is-ascending="isSortAscending"
+ :sort-direction-tool-tip="sortDirectionData.tooltip"
@sortDirectionChange="onDirectionChange"
>
<gl-sorting-item
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index b61996cdcdb..e6c29e24f0c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -53,6 +53,11 @@ export default {
required: false,
default: false,
},
+ allowMultipleScopedLabels: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
variant: {
type: String,
required: false,
@@ -164,6 +169,7 @@ export default {
allowLabelCreate: this.allowLabelCreate,
allowMultiselect: this.allowMultiselect,
allowScopedLabels: this.allowScopedLabels,
+ allowMultipleScopedLabels: this.allowMultipleScopedLabels,
dropdownButtonText: this.dropdownButtonText,
selectedLabels: this.selectedLabels,
labelsFetchPath: this.labelsFetchPath,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
index 0c697e624ab..2dab97826b9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -16,7 +16,7 @@ export const receiveLabelsSuccess = ({ commit }, labels) =>
commit(types.RECEIVE_SET_LABELS_SUCCESS, labels);
export const receiveLabelsFailure = ({ commit }) => {
commit(types.RECEIVE_SET_LABELS_FAILURE);
- createFlash({
+ createAlert({
message: __('Error fetching labels.'),
});
};
@@ -38,7 +38,7 @@ export const requestCreateLabel = ({ commit }) => commit(types.REQUEST_CREATE_LA
export const receiveCreateLabelSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_LABEL_SUCCESS);
export const receiveCreateLabelFailure = ({ commit }) => {
commit(types.RECEIVE_CREATE_LABEL_FAILURE);
- createFlash({
+ createAlert({
message: __('Error creating label.'),
});
};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 43b23994cdf..c85d9befcbb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -94,14 +94,13 @@ export default {
candidateLabel.indeterminate = false;
}
- if (isScopedLabel(candidateLabel)) {
+ if (isScopedLabel(candidateLabel) && !state.allowMultipleScopedLabels) {
const currentActiveScopedLabel = state.labels.find(
({ set, title }) =>
set &&
title !== candidateLabel.title &&
scopedLabelKey({ title }) === scopedLabelKey(candidateLabel),
);
-
if (currentActiveScopedLabel) {
currentActiveScopedLabel.set = false;
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index 5f344ae4214..ce93ad216ec 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -8,7 +8,7 @@ import {
GlLoadingIcon,
} from '@gitlab/ui';
import produce from 'immer';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { workspaceLabelsQueries } from '~/sidebar/constants';
import createLabelMutation from './graphql/create_label.mutation.graphql';
@@ -129,7 +129,7 @@ export default {
this.$emit('hideCreateView');
}
} catch {
- createFlash({ message: errorMessage });
+ createAlert({ message: errorMessage });
}
this.labelCreateInProgress = false;
},
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index 8d3d4d5f86a..1d854505d11 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { workspaceLabelsQueries } from '~/sidebar/constants';
@@ -62,7 +62,7 @@ export default {
},
update: (data) => data.workspace?.labels?.nodes || [],
error() {
- createFlash({ message: __('Error fetching labels.') });
+ createAlert({ message: __('Error fetching labels.') });
},
},
},
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 522fbc07f5e..0e8da7281d8 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -2,7 +2,7 @@
import { debounce } from 'lodash';
import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql';
import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IssuableType } from '~/issues/constants';
@@ -151,7 +151,7 @@ export default {
return data.workspace?.issuable;
},
error() {
- createFlash({ message: __('Error fetching labels.') });
+ createAlert({ message: __('Error fetching labels.') });
},
subscribeToMore: {
document() {
@@ -275,7 +275,7 @@ export default {
});
})
.catch((error) =>
- createFlash({
+ createAlert({
message: __('An error occurred while updating labels.'),
captureError: true,
error,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
index 445817d3e52..eae5e96ac46 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query issueParticipants($fullPath: ID!, $iid: String!) {
+query issueParticipants($fullPath: ID!, $iid: String!, $getStatus: Boolean = false) {
workspace: project(fullPath: $fullPath) {
id
issuable: issue(iid: $iid) {
@@ -9,7 +9,7 @@ query issueParticipants($fullPath: ID!, $iid: String!) {
participants {
nodes {
...User
- ...UserAvailability
+ ...UserAvailability @include(if: $getStatus)
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql
index 05de680ab05..f087ca6c982 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql
@@ -19,7 +19,7 @@ query mergeRequestReviewers($fullPath: ID!, $iid: String!) {
}
}
userPermissions {
- updateMergeRequest
+ adminMergeRequest
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
index 3496d5f4a2e..2781ac71f31 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query getMrParticipants($fullPath: ID!, $iid: String!) {
+query getMrParticipants($fullPath: ID!, $iid: String!, $getStatus: Boolean = false) {
workspace: project(fullPath: $fullPath) {
id
issuable: mergeRequest(iid: $iid) {
@@ -9,7 +9,7 @@ query getMrParticipants($fullPath: ID!, $iid: String!) {
participants {
nodes {
...User
- ...UserAvailability
+ ...UserAvailability @include(if: $getStatus)
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index 257b9f57222..ffd0eea63a1 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -1,8 +1,6 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { setAttributes } from '~/lib/utils/dom_utils';
-import { BIDI_CHARS, BIDI_CHARS_CLASS_LIST, BIDI_CHAR_TOOLTIP } from '../constants';
export default {
directives: {
@@ -27,34 +25,6 @@ export default {
required: true,
},
},
- computed: {
- formattedContent() {
- let { content } = this;
-
- BIDI_CHARS.forEach((bidiChar) => {
- if (content.includes(bidiChar)) {
- content = content.replace(bidiChar, this.wrapBidiChar(bidiChar));
- }
- });
-
- return content;
- },
- },
- methods: {
- wrapBidiChar(bidiChar) {
- const span = document.createElement('span');
-
- setAttributes(span, {
- class: BIDI_CHARS_CLASS_LIST,
- title: BIDI_CHAR_TOOLTIP,
- 'data-testid': 'bidi-wrapper',
- });
-
- span.innerText = bidiChar;
-
- return span.outerHTML;
- },
- },
};
</script>
<template>
@@ -78,7 +48,7 @@ export default {
</div>
<pre
- class="gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-normal"
- ><code><span :id="`LC${number}`" v-safe-html="formattedContent" :lang="language" class="line" data-testid="content"></span></code></pre>
+ class="gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0"
+ ><code><span :id="`LC${number}`" v-safe-html="content" :lang="language" class="line" data-testid="content"></span></code></pre>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
index 30f57f506a6..a28460dd58e 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -1,5 +1,3 @@
-import { __ } from '~/locale';
-
// Language map from Rouge::Lexer to highlight.js
// Rouge::Lexer - We use it on the BE to determine the language of a source file (https://github.com/rouge-ruby/rouge/blob/master/docs/Languages.md).
// Highlight.js - We use it on the FE to highlight the syntax of a source file (https://github.com/highlightjs/highlight.js/tree/main/src/languages).
@@ -139,13 +137,6 @@ export const BIDI_CHARS = [
export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip';
-export const BIDI_CHAR_TOOLTIP = __(
- 'Potentially unwanted character detected: Unicode BiDi Control',
-);
-
-export const HLJS_COMMENT_SELECTOR = 'hljs-comment';
+export const BIDI_CHAR_TOOLTIP = 'Potentially unwanted character detected: Unicode BiDi Control';
export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
-
-export const NPM_URL = 'https://npmjs.com/package';
-export const GEM_URL = 'https://rubygems.org/gems';
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
index 5d24a3d110b..d694adf7147 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
@@ -1,6 +1,8 @@
-import { HLJS_ON_AFTER_HIGHLIGHT } from '../constants';
-import wrapComments from './wrap_comments';
+import wrapChildNodes from './wrap_child_nodes';
import linkDependencies from './link_dependencies';
+import wrapBidiChars from './wrap_bidi_chars';
+
+export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
/**
* Registers our plugins for Highlight.js
@@ -10,7 +12,8 @@ import linkDependencies from './link_dependencies';
* @param {Object} hljs - the Highlight.js instance.
*/
export const registerPlugins = (hljs, fileType, rawContent) => {
- hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapChildNodes });
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapBidiChars });
hljs.addPlugin({
[HLJS_ON_AFTER_HIGHLIGHT]: (result) => linkDependencies(result, fileType, rawContent),
});
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
index dbe6812cf16..49704421d6e 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
@@ -1,16 +1,7 @@
import { escape } from 'lodash';
-import { setAttributes } from '~/lib/utils/dom_utils';
-export const createLink = (href, innerText) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- const rel = 'nofollow noreferrer noopener';
- const link = document.createElement('a');
-
- setAttributes(link, { href: escape(href), rel });
- link.textContent = innerText;
-
- return link.outerHTML;
-};
+export const createLink = (href, innerText) =>
+ `<a href="${escape(href)}" rel="nofollow noreferrer noopener">${escape(innerText)}</a>`;
export const generateHLJSOpenTag = (type, delimiter = '&quot;') =>
`<span class="hljs-${escape(type)}">${delimiter}`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
index 35de8fd13d6..46c9dc38300 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
@@ -1,7 +1,6 @@
-import { joinPaths } from '~/lib/utils/url_utility';
-import { GEM_URL } from '../../constants';
import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+const GEM_URL = 'https://rubygems.org/gems/';
const methodRegex = '.*add_dependency.*|.*add_runtime_dependency.*|.*add_development_dependency.*';
const openTagRegex = generateHLJSOpenTag('string', '(&.*;)');
const closeTagRegex = '&.*</span>';
@@ -24,7 +23,7 @@ const DEPENDENCY_REGEX = new RegExp(
const handleReplace = (method, delimiter, packageName, closeTag, rest) => {
// eslint-disable-next-line @gitlab/require-i18n-strings
const openTag = generateHLJSOpenTag('string linked', delimiter);
- const href = joinPaths(GEM_URL, packageName);
+ const href = `${GEM_URL}${packageName}`;
const packageLink = createLink(href, packageName);
return `${method}${openTag}${packageLink}${closeTag}${rest}`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
index 3c6fc23c138..4bfd5ec2431 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
@@ -1,8 +1,7 @@
import { unescape } from 'lodash';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { NPM_URL } from '../../constants';
import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+const NPM_URL = 'https://npmjs.com/package/';
const attrOpenTag = generateHLJSOpenTag('attr');
const stringOpenTag = generateHLJSOpenTag('string');
const closeTag = '&quot;</span>';
@@ -20,7 +19,7 @@ const DEPENDENCY_REGEX = new RegExp(
const handleReplace = (original, packageName, version, dependenciesToLink) => {
const unescapedPackageName = unescape(packageName);
const unescapedVersion = unescape(version);
- const href = joinPaths(NPM_URL, unescapedPackageName);
+ const href = `${NPM_URL}${unescapedPackageName}`;
const packageLink = createLink(href, unescapedPackageName);
const versionLink = createLink(href, unescapedVersion);
const closeAndOpenTag = `${closeTag}: ${attrOpenTag}`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js
new file mode 100644
index 00000000000..3b6cd96ef78
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js
@@ -0,0 +1,30 @@
+import {
+ BIDI_CHARS,
+ BIDI_CHARS_CLASS_LIST,
+ BIDI_CHAR_TOOLTIP,
+} from '~/vue_shared/components/source_viewer/constants';
+
+/**
+ * Highlight.js plugin for wrapping BIDI chars.
+ * This ensures potentially dangerous BIDI characters are highlighted.
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} Result - an object that represents the highlighted result from Highlight.js
+ */
+
+function wrapBidiChar(bidiChar) {
+ return `<span class="${BIDI_CHARS_CLASS_LIST}" title="${BIDI_CHAR_TOOLTIP}">${bidiChar}</span>`;
+}
+
+export default (result) => {
+ let { value } = result;
+ BIDI_CHARS.forEach((bidiChar) => {
+ if (value.includes(bidiChar)) {
+ value = value.replace(bidiChar, wrapBidiChar(bidiChar));
+ }
+ });
+
+ // eslint-disable-next-line no-param-reassign
+ result.value = value; // Highlight.js expects the result param to be mutated for plugins to work
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
new file mode 100644
index 00000000000..e0ba4b730a7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
@@ -0,0 +1,45 @@
+import { escape } from 'lodash';
+
+/**
+ * Highlight.js plugin for wrapping nodes with the correct selectors to ensure
+ * child-elements are highlighted correctly after we split up the result into chunks and lines.
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} Result - an object that represents the highlighted result from Highlight.js
+ */
+const newlineRegex = /\r?\n/;
+const generateClassName = (suffix) => (suffix ? `hljs-${escape(suffix)}` : '');
+const generateCloseTag = (includeClose) => (includeClose ? '</span>' : '');
+const generateHLJSTag = (kind, content = '', includeClose) =>
+ `<span class="${generateClassName(kind)}">${escape(content)}${generateCloseTag(includeClose)}`;
+
+const format = (node, kind = '') => {
+ let buffer = '';
+
+ if (typeof node === 'string') {
+ buffer += node
+ .split(newlineRegex)
+ .map((newline) => generateHLJSTag(kind, newline, true))
+ .join('\n');
+ } else if (node.kind) {
+ const { children } = node;
+ if (children.length && children.length === 1) {
+ buffer += format(children[0], node.kind);
+ } else {
+ buffer += generateHLJSTag(node.kind);
+ children.forEach((subChild) => {
+ buffer += format(subChild, node.kind);
+ });
+ buffer += `</span>`;
+ }
+ }
+
+ return buffer;
+};
+
+export default (result) => {
+ // NOTE: We're using the private Emitter API here as we expect the Emitter API to be publicly available soon (https://github.com/highlightjs/highlight.js/issues/3621)
+ // eslint-disable-next-line no-param-reassign, no-underscore-dangle
+ result.value = result._emitter.rootNode.children.reduce((val, node) => val + format(node), ''); // Highlight.js expects the result param to be mutated for plugins to work
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
deleted file mode 100644
index 8b52df83fdf..00000000000
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { HLJS_COMMENT_SELECTOR } from '../constants';
-
-const createWrapper = (content) => {
- const span = document.createElement('span');
- span.className = HLJS_COMMENT_SELECTOR;
-
- // eslint-disable-next-line no-unsanitized/property
- span.innerHTML = content;
- return span.outerHTML;
-};
-
-/**
- * Highlight.js plugin for wrapping multi-line comments in the `hljs-comment` class.
- * This ensures that multi-line comments are rendered correctly in the GitLab UI.
- *
- * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
- *
- * @param {Object} Result - an object that represents the highlighted result from Highlight.js
- */
-export default (result) => {
- if (!result.value.includes(HLJS_COMMENT_SELECTOR)) return;
-
- let wrapComment = false;
-
- // eslint-disable-next-line no-param-reassign
- result.value = result.value // Highlight.js expects the result param to be mutated for plugins to work
- .split('\n')
- .map((lineContent) => {
- const includesClosingTag = lineContent.includes('</span>');
- if (lineContent.includes(HLJS_COMMENT_SELECTOR) && !includesClosingTag) {
- wrapComment = true;
- return lineContent;
- }
- const line = wrapComment ? createWrapper(lineContent) : lineContent;
- if (includesClosingTag) {
- wrapComment = false;
- }
- return line;
- })
- .join('\n');
-};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index 9c6c12eac7d..536b2c8a281 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -53,7 +53,7 @@ export default {
},
computed: {
splitContent() {
- return this.content.split('\n');
+ return this.content.split(/\r?\n/);
},
lineNumbers() {
return this.splitContent.length;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js
new file mode 100644
index 00000000000..535e857d7a9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js
@@ -0,0 +1,10 @@
+import { highlight } from './highlight_utils';
+
+/**
+ * A webworker for highlighting large amounts of content with Highlight.js
+ */
+// eslint-disable-next-line no-restricted-globals
+self.addEventListener('message', ({ data: { fileType, content, language } }) => {
+ // eslint-disable-next-line no-restricted-globals
+ self.postMessage(highlight(fileType, content, language));
+});
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
new file mode 100644
index 00000000000..0da57f9e6fa
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
@@ -0,0 +1,15 @@
+import hljs from 'highlight.js/lib/core';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import { registerPlugins } from '../plugins/index';
+
+const initHighlightJs = async (fileType, content, language) => {
+ const languageDefinition = await languageLoader[language]();
+
+ registerPlugins(hljs, fileType, content);
+ hljs.registerLanguage(language, languageDefinition.default);
+};
+
+export const highlight = (fileType, content, language) => {
+ initHighlightJs(fileType, content, language);
+ return hljs.highlight(content, { language }).value;
+};
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
deleted file mode 100644
index ce65266cbc9..00000000000
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
-import { secondsToHours } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
-import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
-
-export default {
- name: 'TimezoneDropdown',
- components: {
- GlDropdown,
- GlDropdownItem,
- GlSearchBoxByType,
- },
- directives: {
- autofocusonshow,
- },
- props: {
- value: {
- type: String,
- required: true,
- default: '',
- },
- timezoneData: {
- type: Array,
- required: true,
- default: () => [],
- },
- },
- data() {
- return {
- searchTerm: '',
- };
- },
- tranlations: {
- noResultsText: __('No matching results'),
- },
- computed: {
- timezones() {
- return this.timezoneData.map((timezone) => ({
- formattedTimezone: this.formatTimezone(timezone),
- identifier: timezone.identifier,
- }));
- },
- filteredResults() {
- const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.timezones.filter((timezone) =>
- timezone.formattedTimezone.toLowerCase().includes(lowerCasedSearchTerm),
- );
- },
- selectedTimezoneLabel() {
- return this.value || __('Select timezone');
- },
- },
- methods: {
- selectTimezone(selectedTimezone) {
- this.$emit('input', selectedTimezone);
- this.searchTerm = '';
- },
- isSelected(timezone) {
- return this.value === timezone.formattedTimezone;
- },
- formatTimezone(item) {
- return `[UTC ${secondsToHours(item.offset)}] ${item.name}`;
- },
- },
-};
-</script>
-<template>
- <gl-dropdown :text="selectedTimezoneLabel" block lazy menu-class="gl-w-full!" v-bind="$attrs">
- <gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus />
- <gl-dropdown-item
- v-for="timezone in filteredResults"
- :key="timezone.formattedTimezone"
- :is-checked="isSelected(timezone)"
- is-check-item
- @click="selectTimezone(timezone)"
- >
- {{ timezone.formattedTimezone }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="!filteredResults.length"
- class="gl-pointer-events-none"
- data-testid="noMatchingResults"
- >
- {{ $options.tranlations.noResultsText }}
- </gl-dropdown-item>
- </gl-dropdown>
-</template>
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
new file mode 100644
index 00000000000..423501265d7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
@@ -0,0 +1,119 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { __ } from '~/locale';
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+import { formatTimezone } from '~/lib/utils/datetime_utility';
+
+export default {
+ name: 'TimezoneDropdown',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ },
+ directives: {
+ autofocusonshow,
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ timezoneData: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ tzValue: this.initialTimezone(this.timezoneData, this.value),
+ };
+ },
+ translations: {
+ noResultsText: __('No matching results'),
+ },
+ computed: {
+ timezones() {
+ return this.timezoneData.map((timezone) => ({
+ formattedTimezone: formatTimezone(timezone),
+ identifier: timezone.identifier,
+ }));
+ },
+ filteredResults() {
+ const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
+ return this.timezones.filter((timezone) =>
+ timezone.formattedTimezone.toLowerCase().includes(lowerCasedSearchTerm),
+ );
+ },
+ selectedTimezoneLabel() {
+ return this.tzValue || __('Select timezone');
+ },
+ timezoneIdentifier() {
+ return this.tzValue
+ ? this.timezones.find((timezone) => timezone.formattedTimezone === this.tzValue).identifier
+ : undefined;
+ },
+ },
+ methods: {
+ selectTimezone(selectedTimezone) {
+ this.tzValue = selectedTimezone.formattedTimezone;
+ this.$emit('input', selectedTimezone);
+ this.searchTerm = '';
+ },
+ isSelected(timezone) {
+ return this.tzValue === timezone.formattedTimezone;
+ },
+ initialTimezone(timezones, value) {
+ if (!value) {
+ return undefined;
+ }
+
+ const initialTimezone = timezones.find((timezone) => timezone.identifier === value);
+
+ if (initialTimezone) {
+ return formatTimezone(initialTimezone);
+ }
+
+ return undefined;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <input
+ v-if="name"
+ id="user_timezone"
+ :name="name"
+ :value="timezoneIdentifier || value"
+ type="hidden"
+ />
+ <gl-dropdown :text="selectedTimezoneLabel" block lazy menu-class="gl-w-full!" v-bind="$attrs">
+ <gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus />
+ <gl-dropdown-item
+ v-for="timezone in filteredResults"
+ :key="timezone.formattedTimezone"
+ :is-checked="isSelected(timezone)"
+ is-check-item
+ @click="selectTimezone(timezone)"
+ >
+ {{ timezone.formattedTimezone }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="!filteredResults.length"
+ class="gl-pointer-events-none"
+ data-testid="noMatchingResults"
+ >
+ {{ $options.translations.noResultsText }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/url_sync.vue b/app/assets/javascripts/vue_shared/components/url_sync.vue
index 925c6008836..bd5b7b77017 100644
--- a/app/assets/javascripts/vue_shared/components/url_sync.vue
+++ b/app/assets/javascripts/vue_shared/components/url_sync.vue
@@ -1,6 +1,9 @@
<script>
import { historyPushState } from '~/lib/utils/common_utils';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { mergeUrlParams, setUrlParams } from '~/lib/utils/url_utility';
+
+export const URL_SET_PARAMS_STRATEGY = 'set';
+export const URL_MERGE_PARAMS_STRATEGY = 'merge';
/**
* Renderless component to update the query string,
@@ -15,6 +18,12 @@ export default {
required: false,
default: null,
},
+ urlParamsUpdateStrategy: {
+ type: String,
+ required: false,
+ default: URL_MERGE_PARAMS_STRATEGY,
+ validator: (value) => [URL_MERGE_PARAMS_STRATEGY, URL_SET_PARAMS_STRATEGY].includes(value),
+ },
},
watch: {
query: {
@@ -29,7 +38,11 @@ export default {
},
methods: {
updateQuery(newQuery) {
- historyPushState(mergeUrlParams(newQuery, window.location.href, { spreadArrays: true }));
+ const url =
+ this.urlParamsUpdateStrategy === URL_SET_PARAMS_STRATEGY
+ ? setUrlParams(this.query, window.location.href, true)
+ : mergeUrlParams(newQuery, window.location.href, { spreadArrays: true });
+ historyPushState(url);
},
},
render() {
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index c1e618620d8..6552a874c3a 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -5,29 +5,29 @@
Sample configuration:
- <user-avatar-image
+ <user-avatar
lazy
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
+ :size="24"
/>
*/
+import { GlTooltip, GlAvatar } from '@gitlab/ui';
+import { isObject } from 'lodash';
import defaultAvatarUrl from 'images/no_avatar.png';
import { __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import UserAvatarImageNew from './user_avatar_image_new.vue';
-import UserAvatarImageOld from './user_avatar_image_old.vue';
+import { placeholderImage } from '~/lazy_loader';
export default {
name: 'UserAvatarImage',
components: {
- UserAvatarImageNew,
- UserAvatarImageOld,
+ GlTooltip,
+ GlAvatar,
},
- mixins: [glFeatureFlagMixin()],
props: {
lazy: {
type: Boolean,
@@ -51,8 +51,7 @@ export default {
},
size: {
type: [Number, Object],
- required: false,
- default: 20,
+ required: true,
},
tooltipText: {
type: String,
@@ -64,22 +63,52 @@ export default {
required: false,
default: 'top',
},
- enforceGlAvatar: {
- type: Boolean,
- required: false,
+ },
+ computed: {
+ // API response sends null when gravatar is disabled and
+ // we provide an empty string when we use it inside user avatar link.
+ // In both cases we should render the defaultAvatarUrl
+ sanitizedSource() {
+ let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ // Only adds the width to the URL if its not a base64 data image
+ if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
+ baseSrc += `?width=${this.maximumSize}`;
+ return baseSrc;
+ },
+ maximumSize() {
+ if (isObject(this.size)) {
+ return Math.max(...Object.values(this.size));
+ }
+
+ return this.size;
+ },
+ resultantSrcAttribute() {
+ return this.lazy ? placeholderImage : this.sanitizedSource;
},
},
};
</script>
<template>
- <user-avatar-image-new
- v-if="glFeatures.glAvatarForAllUserAvatars || enforceGlAvatar"
- v-bind="$props"
- >
- <slot></slot>
- </user-avatar-image-new>
- <user-avatar-image-old v-else v-bind="$props">
- <slot></slot>
- </user-avatar-image-old>
+ <span ref="userAvatar">
+ <gl-avatar
+ :class="{
+ lazy: lazy,
+ [cssClasses]: true,
+ }"
+ :src="resultantSrcAttribute"
+ :data-src="sanitizedSource"
+ :size="size"
+ :alt="imgAlt"
+ />
+
+ <gl-tooltip
+ v-if="tooltipText || $scopedSlots.default"
+ :target="() => $refs.userAvatar"
+ :placement="tooltipPlacement"
+ boundary="window"
+ >
+ <slot>{{ tooltipText }}</slot>
+ </gl-tooltip>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
deleted file mode 100644
index 6bd66981860..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar that
- does not need to link to the user's profile. The image and an optional
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar
- lazy
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :tooltip-text="tooltipText"
- tooltip-placement="top"
- />
-
- */
-
-import { GlTooltip, GlAvatar } from '@gitlab/ui';
-import { isObject } from 'lodash';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { __ } from '~/locale';
-import { placeholderImage } from '~/lazy_loader';
-
-export default {
- name: 'UserAvatarImageNew',
- components: {
- GlTooltip,
- GlAvatar,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- imgSrc: {
- type: String,
- required: false,
- default: defaultAvatarUrl,
- },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: __('user avatar'),
- },
- size: {
- type: [Number, Object],
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- },
- computed: {
- // API response sends null when gravatar is disabled and
- // we provide an empty string when we use it inside user avatar link.
- // In both cases we should render the defaultAvatarUrl
- sanitizedSource() {
- let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
- // Only adds the width to the URL if its not a base64 data image
- if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
- baseSrc += `?width=${this.maximumSize}`;
- return baseSrc;
- },
- maximumSize() {
- if (isObject(this.size)) {
- return Math.max(...Object.values(this.size));
- }
-
- return this.size;
- },
- resultantSrcAttribute() {
- return this.lazy ? placeholderImage : this.sanitizedSource;
- },
- },
-};
-</script>
-
-<template>
- <span ref="userAvatar">
- <gl-avatar
- :class="{
- lazy: lazy,
- [cssClasses]: true,
- }"
- :src="resultantSrcAttribute"
- :data-src="sanitizedSource"
- :size="size"
- :alt="imgAlt"
- />
-
- <gl-tooltip
- v-if="
- tooltipText ||
- $slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
- "
- :target="() => $refs.userAvatar"
- :placement="tooltipPlacement"
- boundary="window"
- >
- <slot>{{ tooltipText }}</slot>
- </gl-tooltip>
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue
deleted file mode 100644
index 6e8c200d5c3..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar that
- does not need to link to the user's profile. The image and an optional
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar-image
- lazy
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :tooltip-text="tooltipText"
- tooltip-placement="top"
- />
-
- */
-
-import { GlTooltip } from '@gitlab/ui';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { __ } from '~/locale';
-import { placeholderImage } from '~/lazy_loader';
-
-export default {
- name: 'UserAvatarImageOld',
- components: {
- GlTooltip,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- imgSrc: {
- type: String,
- required: false,
- default: defaultAvatarUrl,
- },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: __('user avatar'),
- },
- size: {
- type: Number,
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- },
- computed: {
- // API response sends null when gravatar is disabled and
- // we provide an empty string when we use it inside user avatar link.
- // In both cases we should render the defaultAvatarUrl
- sanitizedSource() {
- let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
- // Only adds the width to the URL if its not a base64 data image
- if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
- baseSrc += `?width=${this.size}`;
- return baseSrc;
- },
- resultantSrcAttribute() {
- return this.lazy ? placeholderImage : this.sanitizedSource;
- },
- avatarSizeClass() {
- return `s${this.size}`;
- },
- },
-};
-</script>
-
-<template>
- <span>
- <img
- ref="userAvatarImage"
- :class="{
- lazy: lazy,
- [avatarSizeClass]: true,
- [cssClasses]: true,
- }"
- :src="resultantSrcAttribute"
- :width="size"
- :height="size"
- :alt="imgAlt"
- :data-src="sanitizedSource"
- class="avatar"
- />
- <gl-tooltip
- v-if="
- tooltipText ||
- $slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
- "
- :target="() => $refs.userAvatarImage"
- :placement="tooltipPlacement"
- boundary="window"
- >
- <slot>{{ tooltipText }}</slot>
- </gl-tooltip>
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index f80abed4d69..1a81da3eb0d 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -9,7 +9,7 @@
:link-href="userProfileUrl"
:img-src="userAvatarSrc"
:img-alt="tooltipText"
- :img-size="20"
+ :img-size="32"
:tooltip-text="tooltipText"
:tooltip-placement="top"
:username="username"
@@ -17,17 +17,18 @@
*/
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import UserAvatarLinkNew from './user_avatar_link_new.vue';
-import UserAvatarLinkOld from './user_avatar_link_old.vue';
+import { GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
+import UserAvatarImage from './user_avatar_image.vue';
export default {
- name: 'UserAvatarLink',
+ name: 'UserAvatarLinkNew',
components: {
- UserAvatarLinkNew,
- UserAvatarLinkOld,
+ UserAvatarImage,
+ GlAvatarLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
props: {
lazy: {
type: Boolean,
@@ -56,8 +57,7 @@ export default {
},
imgSize: {
type: [Number, Object],
- required: false,
- default: 20,
+ required: true,
},
tooltipText: {
type: String,
@@ -74,29 +74,43 @@ export default {
required: false,
default: '',
},
- enforceGlAvatar: {
- type: Boolean,
- required: false,
+ },
+ computed: {
+ shouldShowUsername() {
+ return this.username.length > 0;
+ },
+ avatarTooltipText() {
+ return this.shouldShowUsername ? '' : this.tooltipText;
},
},
};
</script>
<template>
- <user-avatar-link-new
- v-if="glFeatures.glAvatarForAllUserAvatars || enforceGlAvatar"
- v-bind="$props"
- >
- <slot></slot>
- <template #avatar-badge>
- <slot name="avatar-badge"></slot>
- </template>
- </user-avatar-link-new>
+ <gl-avatar-link :href="linkHref" class="user-avatar-link">
+ <user-avatar-image
+ :img-src="imgSrc"
+ :img-alt="imgAlt"
+ :css-classes="imgCssClasses"
+ :size="imgSize"
+ :tooltip-text="avatarTooltipText"
+ :tooltip-placement="tooltipPlacement"
+ :lazy="lazy"
+ >
+ <slot></slot>
+ </user-avatar-image>
+
+ <span
+ v-if="shouldShowUsername"
+ v-gl-tooltip
+ :title="tooltipText"
+ :tooltip-placement="tooltipPlacement"
+ class="gl-ml-3"
+ data-testid="user-avatar-link-username"
+ >
+ {{ username }}
+ </span>
- <user-avatar-link-old v-else v-bind="$props">
- <slot></slot>
- <template #avatar-badge>
- <slot name="avatar-badge"></slot>
- </template>
- </user-avatar-link-old>
+ <slot name="avatar-badge"></slot>
+ </gl-avatar-link>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue
deleted file mode 100644
index 83551c689c4..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar wrapped in
- a clickable link (likely to the user's profile). The link, image, and
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar-link
- :link-href="userProfileUrl"
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :img-size="20"
- :tooltip-text="tooltipText"
- :tooltip-placement="top"
- :username="username"
- />
-
-*/
-
-import { GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
-import UserAvatarImage from './user_avatar_image.vue';
-
-export default {
- name: 'UserAvatarLinkNew',
- components: {
- UserAvatarImage,
- GlAvatarLink,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- linkHref: {
- type: String,
- required: false,
- default: '',
- },
- imgSrc: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: '',
- },
- imgCssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgSize: {
- type: [Number, Object],
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- username: {
- type: String,
- required: false,
- default: '',
- },
- enforceGlAvatar: {
- type: Boolean,
- required: false,
- },
- },
- computed: {
- shouldShowUsername() {
- return this.username.length > 0;
- },
- avatarTooltipText() {
- return this.shouldShowUsername ? '' : this.tooltipText;
- },
- },
-};
-</script>
-
-<template>
- <gl-avatar-link :href="linkHref" class="user-avatar-link">
- <user-avatar-image
- :img-src="imgSrc"
- :img-alt="imgAlt"
- :css-classes="imgCssClasses"
- :size="imgSize"
- :tooltip-text="avatarTooltipText"
- :tooltip-placement="tooltipPlacement"
- :lazy="lazy"
- :enforce-gl-avatar="enforceGlAvatar"
- >
- <slot></slot>
- </user-avatar-image>
-
- <span
- v-if="shouldShowUsername"
- v-gl-tooltip
- :title="tooltipText"
- :tooltip-placement="tooltipPlacement"
- class="gl-ml-3"
- data-testid="user-avatar-link-username"
- >
- {{ username }}
- </span>
-
- <slot name="avatar-badge"></slot>
- </gl-avatar-link>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue
deleted file mode 100644
index c2e46e61e1b..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar wrapped in
- a clickable link (likely to the user's profile). The link, image, and
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar-link
- :link-href="userProfileUrl"
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :img-size="20"
- :tooltip-text="tooltipText"
- :tooltip-placement="top"
- :username="username"
- />
-
-*/
-
-import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-import UserAvatarImage from './user_avatar_image.vue';
-
-export default {
- name: 'UserAvatarLinkOld',
- components: {
- GlLink,
- UserAvatarImage,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- linkHref: {
- type: String,
- required: false,
- default: '',
- },
- imgSrc: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: '',
- },
- imgCssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgSize: {
- type: Number,
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- username: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- shouldShowUsername() {
- return this.username.length > 0;
- },
- avatarTooltipText() {
- return this.shouldShowUsername ? '' : this.tooltipText;
- },
- },
-};
-</script>
-
-<template>
- <span>
- <gl-link :href="linkHref" class="user-avatar-link">
- <user-avatar-image
- :img-src="imgSrc"
- :img-alt="imgAlt"
- :css-classes="imgCssClasses"
- :size="imgSize"
- :tooltip-text="avatarTooltipText"
- :tooltip-placement="tooltipPlacement"
- :lazy="lazy"
- >
- <slot></slot>
- </user-avatar-image>
-
- <span
- v-if="shouldShowUsername"
- v-gl-tooltip
- :title="tooltipText"
- :tooltip-placement="tooltipPlacement"
- data-testid="user-avatar-link-username"
- >
- {{ username }}
- </span>
- <slot name="avatar-badge"></slot>
- </gl-link>
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
index 9da298ad705..231f5ff3d1f 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
@@ -1,6 +1,5 @@
<script>
import { GlButton } from '@gitlab/ui';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { sprintf, __ } from '~/locale';
import UserAvatarLink from './user_avatar_link.vue';
@@ -9,7 +8,6 @@ export default {
UserAvatarLink,
GlButton,
},
- mixins: [glFeatureFlagMixin()],
props: {
items: {
type: Array,
@@ -22,8 +20,7 @@ export default {
},
imgSize: {
type: [Number, Object],
- required: false,
- default: 20,
+ required: true,
},
emptyText: {
type: String,
@@ -59,9 +56,6 @@ export default {
return sprintf(__('%{count} more'), { count });
},
- imgCssClasses() {
- return this.glFeatures.glAvatarForAllUserAvatars ? 'gl-mr-3' : '';
- },
},
methods: {
expand() {
@@ -85,7 +79,7 @@ export default {
:img-alt="item.name"
:tooltip-text="item.name"
:img-size="imgSize"
- :img-css-classes="imgCssClasses"
+ img-css-classes="gl-mr-3"
/>
<template v-if="hasBreakpoint">
<gl-button v-if="hasHiddenItems" variant="link" @click="expand">
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 4b39a8e45bb..80c1fcbacfa 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -10,7 +10,7 @@ import {
GlAvatarLabeled,
} from '@gitlab/ui';
import { glEmojiTag } from '~/emoji';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { followUser, unfollowUser } from '~/rest_api';
import { isUserBusy } from '~/set_status_modal/utils';
import Tracking from '~/tracking';
@@ -83,6 +83,8 @@ export default {
return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
} else if (this.user.status.message_html) {
return this.user.status.message_html;
+ } else if (this.user.status.emoji) {
+ return glEmojiTag(this.user.status.emoji);
}
return '';
@@ -139,8 +141,9 @@ export default {
await followUser(this.user.id);
this.$emit('follow');
} catch (error) {
- createFlash({
- message: I18N_ERROR_FOLLOW,
+ const message = error.response?.data?.message || I18N_ERROR_FOLLOW;
+ createAlert({
+ message,
error,
captureError: true,
});
@@ -159,7 +162,7 @@ export default {
await unfollowUser(this.user.id);
this.$emit('unfollow');
} catch (error) {
- createFlash({
+ createAlert({
message: I18N_ERROR_UNFOLLOW,
error,
captureError: true,
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 3180bd0d283..86a99b8f0ed 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
@@ -103,6 +103,7 @@ export default {
return {
iid: this.iid,
fullPath: this.fullPath,
+ getStatus: true,
};
},
update(data) {
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index b6d69faebb5..a851f84ed2f 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -93,3 +93,6 @@ export const confidentialityInfoText = (workspaceType, issuableType) =>
: __('at least the Reporter role'),
},
);
+
+export const EDITING_MODE_MARKDOWN_FIELD = 'markdownField';
+export const EDITING_MODE_CONTENT_EDITOR = 'contentEditor';
diff --git a/app/assets/javascripts/vue_shared/directives/safe_html.js b/app/assets/javascripts/vue_shared/directives/safe_html.js
new file mode 100644
index 00000000000..450c7fc1bc5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/directives/safe_html.js
@@ -0,0 +1,25 @@
+import { sanitize } from '~/lib/dompurify';
+
+// Mitigate against future dompurify mXSS bypasses by
+// avoiding additional serialize/parse round trip.
+// See https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1782
+// and https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2127
+// for more details.
+const DEFAULT_CONFIG = {
+ RETURN_DOM_FRAGMENT: true,
+};
+
+const transform = (el, binding) => {
+ if (binding.oldValue !== binding.value) {
+ const config = { ...DEFAULT_CONFIG, ...(binding.arg ?? {}) };
+
+ el.textContent = '';
+
+ el.appendChild(sanitize(binding.value, config));
+ }
+};
+
+export default {
+ bind: transform,
+ update: transform,
+};
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index 232749a2d01..624ae7027d5 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -1,11 +1,13 @@
<script>
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import NewTopLevelGroupAlert from '~/groups/components/new_top_level_group_alert.vue';
import LegacyContainer from './components/legacy_container.vue';
import WelcomePage from './components/welcome.vue';
export default {
components: {
+ NewTopLevelGroupAlert,
GlBreadcrumb,
GlIcon,
WelcomePage,
@@ -79,6 +81,14 @@ export default {
shouldVerify() {
return this.verificationRequired && !this.verificationCompleted;
},
+
+ showNewTopLevelGroupAlert() {
+ if (this.activePanel.detailProps === undefined) {
+ return false;
+ }
+
+ return this.activePanel.detailProps.parentGroupName === '';
+ },
},
created() {
@@ -130,6 +140,7 @@ export default {
<slot name="extra-description"></slot>
</div>
<div class="col-lg-9">
+ <new-top-level-group-alert v-if="showNewTopLevelGroupAlert" />
<gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs" />
<legacy-container :key="activePanel.name" :selector="activePanel.selector" />
</div>
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
index e0669b3ed27..a4fb30a03a1 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
@@ -1,6 +1,6 @@
<script>
import { reportTypeToSecurityReportTypeEnum } from 'ee_else_ce/vue_shared/security_reports/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
@@ -67,7 +67,7 @@ export default {
},
methods: {
showError(error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.apiError,
captureError: true,
error,
diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
index f6d85599dba..0e1975e1c09 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import ReportSection from '~/reports/components/report_section.vue';
import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
@@ -160,7 +160,7 @@ export default {
this.fetchCounts();
},
showError(error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.apiError,
captureError: true,
error,
diff --git a/app/assets/javascripts/webhooks/components/form_url_app.vue b/app/assets/javascripts/webhooks/components/form_url_app.vue
new file mode 100644
index 00000000000..5ec16d4ba15
--- /dev/null
+++ b/app/assets/javascripts/webhooks/components/form_url_app.vue
@@ -0,0 +1,134 @@
+<script>
+import { isEmpty } from 'lodash';
+import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+
+import FormUrlMaskItem from './form_url_mask_item.vue';
+
+export default {
+ components: {
+ FormUrlMaskItem,
+ GlFormGroup,
+ GlFormInput,
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlLink,
+ },
+ props: {
+ initialUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ initialUrlVariables: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ maskEnabled: !isEmpty(this.initialUrlVariables),
+ url: this.initialUrl,
+ items: isEmpty(this.initialUrlVariables) ? [{}] : this.initialUrlVariables,
+ };
+ },
+ computed: {
+ maskedUrl() {
+ if (!this.url) {
+ return null;
+ }
+
+ let maskedUrl = this.url;
+
+ this.items.forEach(({ key, value }) => {
+ if (!key || !value) {
+ return;
+ }
+
+ const replacementExpression = new RegExp(value, 'g');
+ maskedUrl = maskedUrl.replace(replacementExpression, `{${key}}`);
+ });
+
+ return maskedUrl;
+ },
+ },
+ methods: {
+ onItemInput({ index, key, value }) {
+ this.$set(this.items, index, { key, value });
+ },
+ addItem() {
+ this.items.push({});
+ },
+ removeItem(index) {
+ this.items.splice(index, 1);
+ },
+ },
+ i18n: {
+ addItem: s__('Webhooks|+ Mask another portion of URL'),
+ radioFullUrlText: s__('Webhooks|Show full URL'),
+ radioMaskUrlText: s__('Webhooks|Mask portions of URL'),
+ radioMaskUrlHelp: s__('Webhooks|Do not show sensitive data such as tokens in the UI.'),
+ urlDescription: s__(
+ 'Webhooks|URL must be percent-encoded if it contains one or more special characters.',
+ ),
+ urlLabel: __('URL'),
+ urlPlaceholder: 'http://example.com/trigger-ci.json',
+ urlPreview: s__('Webhooks|URL preview'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-form-group
+ :label="$options.i18n.urlLabel"
+ label-for="webhook-url"
+ :description="$options.i18n.urlDescription"
+ >
+ <gl-form-input
+ id="webhook-url"
+ v-model="url"
+ name="hook[url]"
+ :placeholder="$options.i18n.urlPlaceholder"
+ data-testid="form-url"
+ />
+ </gl-form-group>
+ <div class="gl-mt-5">
+ <gl-form-radio-group v-model="maskEnabled">
+ <gl-form-radio :value="false">{{ $options.i18n.radioFullUrlText }}</gl-form-radio>
+ <gl-form-radio :value="true"
+ >{{ $options.i18n.radioMaskUrlText }}
+ <template #help>
+ {{ $options.i18n.radioMaskUrlHelp }}
+ </template>
+ </gl-form-radio>
+ </gl-form-radio-group>
+
+ <div v-if="maskEnabled" class="gl-ml-6" data-testid="url-mask-section">
+ <form-url-mask-item
+ v-for="({ key, value }, index) in items"
+ :key="index"
+ :index="index"
+ :item-key="key"
+ :item-value="value"
+ @input="onItemInput"
+ @remove="removeItem"
+ />
+ <div class="gl-mb-5">
+ <gl-link @click="addItem">{{ $options.i18n.addItem }}</gl-link>
+ </div>
+
+ <gl-form-group :label="$options.i18n.urlPreview" label-for="webhook-url-preview">
+ <gl-form-input
+ id="webhook-url-preview"
+ :value="maskedUrl"
+ readonly
+ name="hook[url]"
+ data-testid="form-url-preview"
+ />
+ </gl-form-group>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
new file mode 100644
index 00000000000..3b75f9b6c0d
--- /dev/null
+++ b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
@@ -0,0 +1,90 @@
+<script>
+import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlFormGroup,
+ GlFormInput,
+ },
+ props: {
+ index: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ itemKey: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ itemValue: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ keyInputId() {
+ return this.inputId('key');
+ },
+ valueInputId() {
+ return this.inputId('value');
+ },
+ },
+ methods: {
+ inputId(type) {
+ return `webhook-url-mask-item-${type}-${this.index}`;
+ },
+ inputName(type) {
+ return `hook[url_variables][][${type}]`;
+ },
+ onKeyInput(key) {
+ this.$emit('input', { index: this.index, key, value: this.itemValue });
+ },
+ onValueInput(value) {
+ this.$emit('input', { index: this.index, key: this.itemKey, value });
+ },
+ onRemoveClick() {
+ this.$emit('remove', this.index);
+ },
+ },
+ i18n: {
+ keyLabel: s__('Webhooks|How it looks in the UI'),
+ valueLabel: s__('Webhooks|Sensitive portion of URL'),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-flex-end gl-gap-3 gl-mb-3">
+ <gl-form-group
+ :label="$options.i18n.valueLabel"
+ :label-for="valueInputId"
+ class="gl-flex-grow-1 gl-mb-0"
+ data-testid="mask-item-value"
+ >
+ <gl-form-input
+ :id="valueInputId"
+ :name="inputName('value')"
+ :value="itemValue"
+ @input="onValueInput"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.i18n.keyLabel"
+ :label-for="keyInputId"
+ class="gl-flex-grow-1 gl-mb-0"
+ data-testid="mask-item-key"
+ >
+ <gl-form-input
+ :id="keyInputId"
+ :name="inputName('key')"
+ :value="itemKey"
+ @input="onKeyInput"
+ />
+ </gl-form-group>
+ <gl-button icon="remove" :aria-label="__('Remove')" @click="onRemoveClick" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/webhooks/index.js b/app/assets/javascripts/webhooks/index.js
new file mode 100644
index 00000000000..1b2b33e44c1
--- /dev/null
+++ b/app/assets/javascripts/webhooks/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import FormUrlApp from './components/form_url_app.vue';
+
+export default () => {
+ const el = document.querySelector('.js-vue-webhook-form');
+
+ if (!el) {
+ return null;
+ }
+
+ const { url: initialUrl, urlVariables } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'WebhookFormRoot',
+ render(createElement) {
+ return createElement(FormUrlApp, {
+ props: {
+ initialUrl,
+ initialUrlVariables: urlVariables ? JSON.parse(urlVariables) : undefined,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 4585426edaa..4d6a27f61ac 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -10,7 +10,7 @@ import {
GlDropdownDivider,
GlIntersectionObserver,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
@@ -126,6 +126,9 @@ export default {
},
},
computed: {
+ assigneesTitleId() {
+ return uniqueId('assignees-title-');
+ },
searchUsers() {
return this.users.nodes.map((node) => addClass({ ...node, ...node.user }));
},
@@ -139,9 +142,6 @@ export default {
property: `type_${this.workItemType}`,
};
},
- assigneeListEmpty() {
- return this.assignees.length === 0;
- },
containerClass() {
return !this.isEditing ? 'gl-shadow-none!' : '';
},
@@ -296,12 +296,14 @@ export default {
<template>
<div class="form-row gl-mb-5 work-item-assignees gl-relative gl-flex-nowrap">
<span
+ :id="assigneesTitleId"
class="gl-font-weight-bold col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
data-testid="assignees-title"
>{{ assigneeText }}</span
>
<gl-token-selector
ref="tokenSelector"
+ :aria-labelledby="assigneesTitleId"
:selected-tokens="localAssignees"
:container-class="containerClass"
:class="{ 'gl-hover-border-gray-200': canUpdate }"
@@ -319,7 +321,7 @@ export default {
>
<template #empty-placeholder>
<div
- class="add-assignees gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-gray-300 gl-pr-4 gl-pl-2 gl-top-2"
+ class="add-assignees gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-secondary gl-pr-4 gl-pl-2 gl-top-2"
data-testid="empty-state"
>
<gl-icon name="profile" />
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 c2e4a50fe31..57babe4569d 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -5,6 +5,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { __, s__ } from '~/locale';
+import EditedAt from '~/issues/show/components/edited.vue';
import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import workItemQuery from '../graphql/work_item.query.graphql';
@@ -16,6 +17,7 @@ export default {
SafeHtml: GlSafeHtmlDirective,
},
components: {
+ EditedAt,
GlButton,
GlFormGroup,
MarkdownField,
@@ -89,6 +91,15 @@ export default {
workItemType() {
return this.workItem?.workItemType?.name;
},
+ lastEditedAt() {
+ return this.workItemDescription?.lastEditedAt;
+ },
+ lastEditedByName() {
+ return this.workItemDescription?.lastEditedBy?.name;
+ },
+ lastEditedByPath() {
+ return this.workItemDescription?.lastEditedBy?.webPath;
+ },
markdownPreviewPath() {
return `${gon.relative_url_root || ''}/${this.fullPath}/preview_markdown?target_type=${
this.workItemType
@@ -228,12 +239,18 @@ export default {
class="gl-ml-auto"
icon="pencil"
data-testid="edit-description"
- :aria-label="__('Edit')"
+ :aria-label="__('Edit description')"
@click="startEditing"
/>
</div>
<div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
<div v-else v-safe-html="descriptionHtml" class="md gl-mb-5 gl-min-h-8"></div>
+ <edited-at
+ v-if="lastEditedAt"
+ :updated-at="lastEditedAt"
+ :updated-by-name="lastEditedByName"
+ :updated-by-path="lastEditedByPath"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 3d25df9fcb8..af9b8c6101a 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -7,7 +7,10 @@ import {
GlBadge,
GlButton,
GlTooltipDirective,
+ GlEmptyState,
} from '@gitlab/ui';
+import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg';
+import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@@ -20,11 +23,14 @@ import {
WIDGET_TYPE_WEIGHT,
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_VIEWED_STORAGE_KEY,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_ITERATION,
} from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
+import workItemAssigneesSubscription from '../graphql/work_item_assignees.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
@@ -35,6 +41,7 @@ import WorkItemDescription from './work_item_description.vue';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
+import WorkItemMilestone from './work_item_milestone.vue';
import WorkItemInformation from './work_item_information.vue';
export default {
@@ -49,6 +56,7 @@ export default {
GlLoadingIcon,
GlSkeletonLoader,
GlIcon,
+ GlEmptyState,
WorkItemAssignees,
WorkItemActions,
WorkItemDescription,
@@ -60,6 +68,8 @@ export default {
WorkItemInformation,
LocalStorageSync,
WorkItemTypeIcon,
+ WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
+ WorkItemMilestone,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -82,6 +92,7 @@ export default {
data() {
return {
error: undefined,
+ updateError: undefined,
workItem: {},
showInfoBanner: true,
updateInProgress: false,
@@ -100,9 +111,10 @@ export default {
},
error() {
this.error = this.$options.i18n.fetchError;
+ document.title = s__('404|Not found');
},
result() {
- if (!this.isModal) {
+ if (!this.isModal && this.workItem.project) {
const path = this.workItem.project?.fullPath
? ` · ${this.workItem.project.fullPath}`
: '';
@@ -127,7 +139,18 @@ export default {
};
},
skip() {
- return !this.workItemDueDate;
+ return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
+ },
+ },
+ {
+ document: workItemAssigneesSubscription,
+ variables() {
+ return {
+ issuableId: this.workItemId,
+ };
+ },
+ skip() {
+ return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
},
},
],
@@ -152,37 +175,44 @@ export default {
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
+ parentWorkItem() {
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
+ },
+ parentWorkItemConfidentiality() {
+ return this.parentWorkItem?.confidential;
+ },
+ parentUrl() {
+ return `../../issues/${this.parentWorkItem?.iid}`;
+ },
+ workItemIconName() {
+ return this.workItem?.workItemType?.iconName;
+ },
+ noAccessSvgPath() {
+ return `data:image/svg+xml;utf8,${encodeURIComponent(noAccessSvg)}`;
+ },
hasDescriptionWidget() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
+ return this.isWidgetPresent(WIDGET_TYPE_DESCRIPTION);
},
workItemAssignees() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES);
+ return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
},
workItemLabels() {
- return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
+ return this.isWidgetPresent(WIDGET_TYPE_LABELS);
},
workItemDueDate() {
- return this.workItem?.widgets?.find(
- (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE,
- );
+ return this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
},
workItemWeight() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
+ return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
},
workItemHierarchy() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
},
- parentWorkItem() {
- return this.workItemHierarchy?.parent;
+ workItemIteration() {
+ return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
},
- parentWorkItemConfidentiality() {
- return this.parentWorkItem?.confidential;
- },
- parentUrl() {
- return `../../issues/${this.parentWorkItem?.iid}`;
- },
- workItemIconName() {
- return this.workItem?.workItemType?.iconName;
+ workItemMilestone() {
+ return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
},
},
beforeDestroy() {
@@ -191,6 +221,9 @@ export default {
this.dismissBanner();
},
methods: {
+ isWidgetPresent(type) {
+ return this.workItem?.widgets?.find((widget) => widget.type === type);
+ },
dismissBanner() {
this.showInfoBanner = false;
},
@@ -236,7 +269,7 @@ export default {
},
)
.catch((error) => {
- this.error = error.message;
+ this.updateError = error.message;
})
.finally(() => {
this.updateInProgress = false;
@@ -249,8 +282,13 @@ export default {
<template>
<section class="gl-pt-5">
- <gl-alert v-if="error" class="gl-mb-3" variant="danger" @dismiss="error = undefined">
- {{ error }}
+ <gl-alert
+ v-if="updateError"
+ class="gl-mb-3"
+ variant="danger"
+ @dismiss="updateError = undefined"
+ >
+ {{ updateError }}
</gl-alert>
<div v-if="workItemLoading" class="gl-max-w-26 gl-py-5">
@@ -289,7 +327,7 @@ export default {
</li>
</ul>
<work-item-type-icon
- v-else
+ v-else-if="!error"
:work-item-icon-name="workItemIconName"
:work-item-type="workItemType && workItemType.toUpperCase()"
show-text
@@ -316,7 +354,7 @@ export default {
:is-parent-confidential="parentWorkItemConfidentiality"
@deleteWorkItem="$emit('deleteWorkItem', workItemType)"
@toggleWorkItemConfidentiality="toggleConfidentiality"
- @error="error = $event"
+ @error="updateError = $event"
/>
<gl-button
v-if="isModal"
@@ -332,24 +370,25 @@ export default {
:storage-key="$options.WORK_ITEM_VIEWED_STORAGE_KEY"
>
<work-item-information
- v-if="showInfoBanner"
+ v-if="showInfoBanner && !error"
:show-info-banner="showInfoBanner"
@work-item-banner-dismissed="dismissBanner"
/>
</local-storage-sync>
<work-item-title
+ v-if="workItem.title"
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
- @error="error = $event"
+ @error="updateError = $event"
/>
<work-item-state
:work-item="workItem"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
- @error="error = $event"
+ @error="updateError = $event"
/>
<work-item-assignees
v-if="workItemAssignees"
@@ -360,24 +399,33 @@ export default {
:work-item-type="workItemType"
:can-invite-members="workItemAssignees.canInviteMembers"
:full-path="fullPath"
- @error="error = $event"
+ @error="updateError = $event"
+ />
+ <work-item-labels
+ v-if="workItemLabels"
+ :work-item-id="workItem.id"
+ :can-update="canUpdate"
+ :full-path="fullPath"
+ @error="updateError = $event"
+ />
+ <work-item-due-date
+ v-if="workItemDueDate"
+ :can-update="canUpdate"
+ :due-date="workItemDueDate.dueDate"
+ :start-date="workItemDueDate.startDate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
/>
<template v-if="workItemsMvc2Enabled">
- <work-item-labels
- v-if="workItemLabels"
+ <work-item-milestone
+ v-if="workItemMilestone"
:work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.nodes[0]"
+ :work-item-type="workItemType"
:can-update="canUpdate"
:full-path="fullPath"
- @error="error = $event"
- />
- <work-item-due-date
- v-if="workItemDueDate"
- :can-update="canUpdate"
- :due-date="workItemDueDate.dueDate"
- :start-date="workItemDueDate.startDate"
- :work-item-id="workItem.id"
- :work-item-type="workItemType"
- @error="error = $event"
+ @error="updateError = $event"
/>
</template>
<work-item-weight
@@ -387,14 +435,31 @@ export default {
:weight="workItemWeight.weight"
:work-item-id="workItem.id"
:work-item-type="workItemType"
- @error="error = $event"
+ @error="updateError = $event"
/>
+ <template v-if="workItemsMvc2Enabled">
+ <work-item-iteration
+ v-if="workItemIteration"
+ class="gl-mb-5"
+ :iteration="workItemIteration.iteration"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
+ />
+ </template>
<work-item-description
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
:full-path="fullPath"
class="gl-pt-5"
- @error="error = $event"
+ @error="updateError = $event"
+ />
+ <gl-empty-state
+ v-if="error"
+ :title="$options.i18n.fetchErrorTitle"
+ :description="error"
+ :svg-path="noAccessSvgPath"
/>
</template>
</section>
diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue
index 05f8fa8f5e1..eae11c2bb2f 100644
--- a/app/assets/javascripts/work_items/components/work_item_due_date.vue
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -198,7 +198,7 @@ export default {
label-cols="3"
label-cols-lg="2"
>
- <span v-if="isReadonlyWithNoDates" class="gl-text-gray-400 gl-ml-4">
+ <span v-if="isReadonlyWithNoDates" class="gl-text-secondary gl-ml-4">
{{ $options.i18n.none }}
</span>
<div v-else class="gl-display-flex gl-flex-wrap gl-gap-5">
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index b8b5198be57..05077862690 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -1,16 +1,22 @@
<script>
import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, uniqueId, without } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import workItemQuery from '../graphql/work_item.query.graphql';
-import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
-import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_LABELS } from '../constants';
+import {
+ i18n,
+ I18N_WORK_ITEM_ERROR_FETCHING_LABELS,
+ TRACKING_CATEGORY_SHOW,
+ WIDGET_TYPE_LABELS,
+} from '../constants';
function isTokenSelectorElement(el) {
return el?.classList.contains('gl-label-close') || el?.classList.contains('dropdown-item');
@@ -52,6 +58,8 @@ export default {
localLabels: [],
searchKey: '',
searchLabels: [],
+ addLabelIds: [],
+ removeLabelIds: [],
};
},
apollo: {
@@ -68,13 +76,21 @@ export default {
error() {
this.$emit('error', i18n.fetchError);
},
+ subscribeToMore: {
+ document: workItemLabelsSubscription,
+ variables() {
+ return {
+ issuableId: this.workItemId,
+ };
+ },
+ },
},
searchLabels: {
query: labelSearchQuery,
variables() {
return {
fullPath: this.fullPath,
- search: this.searchKey,
+ searchTerm: this.searchKey,
};
},
skip() {
@@ -84,11 +100,14 @@ export default {
return data.workspace?.labels?.nodes.map((node) => addClass({ ...node, ...node.label }));
},
error() {
- this.$emit('error', i18n.fetchError);
+ this.$emit('error', I18N_WORK_ITEM_ERROR_FETCHING_LABELS);
},
},
},
computed: {
+ labelsTitleId() {
+ return uniqueId('labels-title-');
+ },
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
@@ -97,10 +116,7 @@ export default {
};
},
allowScopedLabels() {
- return this.labelsWidget.allowScopedLabels;
- },
- listEmpty() {
- return this.labels.length === 0;
+ return this.labelsWidget?.allowsScopedLabels;
},
containerClass() {
return !this.isEditing ? 'gl-shadow-none!' : '';
@@ -109,10 +125,10 @@ export default {
return this.$apollo.queries.searchLabels.loading;
},
labelsWidget() {
- return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
},
labels() {
- return this.labelsWidget?.nodes || [];
+ return this.labelsWidget?.labels?.nodes || [];
},
},
watch: {
@@ -131,44 +147,74 @@ export default {
},
removeLabel({ id }) {
this.localLabels = this.localLabels.filter((label) => label.id !== id);
+ this.removeLabelIds.push(id);
+ this.setLabels();
},
- setLabels(event) {
+ async setLabels() {
+ if (this.addLabelIds.length === 0 && this.removeLabelIds.length === 0) return;
+
this.searchKey = '';
- if (isTokenSelectorElement(event.relatedTarget) || !this.isEditing) return;
this.isEditing = false;
- this.$apollo
- .mutate({
- mutation: localUpdateWorkItemMutation,
+ try {
+ const {
+ data: {
+ workItemUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
- labels: this.localLabels,
+ labelsWidget: {
+ addLabelIds: this.addLabelIds,
+ removeLabelIds: this.removeLabelIds,
+ },
},
},
- })
- .catch((e) => {
- this.$emit('error', e);
});
- this.track('updated_labels');
+
+ if (errors.length > 0) {
+ this.throwUpdateError();
+ return;
+ }
+
+ this.addLabelIds = [];
+ this.removeLabelIds = [];
+
+ this.track('updated_labels');
+ } catch {
+ this.throwUpdateError();
+ }
+ },
+ throwUpdateError() {
+ this.$emit('error', i18n.updateError);
+ // If mutation is rejected, we're rolling back to initial state
+ this.localLabels = this.labels.map(addClass);
+ this.addLabelIds = [];
+ this.removeLabelIds = [];
+ },
+ handleBlur(event) {
+ if (isTokenSelectorElement(event.relatedTarget) || !this.isEditing) return;
+ this.setLabels();
},
handleFocus() {
this.isEditing = true;
this.searchStarted = true;
},
async focusTokenSelector(labels) {
- if (this.allowScopedLabels) {
- const newLabel = labels[labels.length - 1];
- const existingLabels = labels.slice(0, labels.length - 1);
-
- const newLabelKey = scopedLabelKey(newLabel);
+ const labelsToAdd = without(labels, ...this.localLabels).map((label) => label.id);
+ const labelsToRemove = without(this.localLabels, ...labels).map((label) => label.id);
- const removeLabelsWithSameScope = existingLabels.filter((label) => {
- const sameKey = newLabelKey === scopedLabelKey(label);
- return !sameKey;
- });
+ if (labelsToAdd.length > 0) {
+ this.addLabelIds.push(...labelsToAdd);
+ }
- this.localLabels = [...removeLabelsWithSameScope, newLabel];
+ if (labelsToRemove.length > 0) {
+ this.removeLabelIds.push(...labelsToRemove);
}
+
+ this.localLabels = labels;
+
this.handleFocus();
await this.$nextTick();
this.$refs.tokenSelector.focusTextInput();
@@ -194,13 +240,15 @@ export default {
<template>
<div class="form-row gl-mb-5 work-item-labels gl-relative gl-flex-nowrap">
<span
+ :id="labelsTitleId"
class="gl-font-weight-bold gl-mt-2 col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
data-testid="labels-title"
>{{ __('Labels') }}</span
>
<gl-token-selector
ref="tokenSelector"
- v-model="localLabels"
+ :selected-tokens="localLabels"
+ :aria-labelledby="labelsTitleId"
:container-class="containerClass"
:dropdown-items="searchLabels"
:loading="isLoading"
@@ -210,13 +258,13 @@ export default {
@input="focusTokenSelector"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
- @blur="setLabels"
+ @blur="handleBlur"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
>
<template #empty-placeholder>
<div
- class="add-labels gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-gray-400 gl-pr-4 gl-top-2"
+ class="add-labels gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-secondary gl-pr-4 gl-top-2"
data-testid="empty-state"
>
<span v-if="canUpdate" class="gl-ml-2">{{ __('Add labels') }}</span>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
index 8f31b07b6a3..37aa48be6e5 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/index.js
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -16,7 +16,13 @@ export default function initWorkItemLinks() {
return;
}
- const { projectPath, wiHasIssueWeightsFeature, iid } = workItemLinksRoot.dataset;
+ const {
+ projectPath,
+ wiHasIssueWeightsFeature,
+ iid,
+ wiHasIterationsFeature,
+ projectNamespace,
+ } = workItemLinksRoot.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -31,6 +37,8 @@ export default function initWorkItemLinks() {
iid,
fullPath: projectPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
+ hasIterationsFeature: wiHasIterationsFeature,
+ projectNamespace,
},
render: (createElement) =>
createElement('work-item-links', {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index 840fd910272..0d3e951de7e 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -5,7 +5,7 @@ import { s__ } from '~/locale';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
-import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
+import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { isMetaKey } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -59,7 +59,7 @@ export default {
},
},
parentIssue: {
- query: issueConfidentialQuery,
+ query: getIssueDetailsQuery,
variables() {
return {
fullPath: this.projectPath,
@@ -86,6 +86,9 @@ export default {
confidential() {
return this.parentIssue?.confidential || this.workItem?.confidential || false;
},
+ issuableIteration() {
+ return this.parentIssue?.iteration;
+ },
children() {
return (
this.workItem?.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.children
@@ -257,7 +260,7 @@ export default {
class="gl-display-inline-flex gl-align-items-center gl-line-height-24 gl-ml-3"
data-testid="children-count"
>
- <gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-2 gl-text-gray-500" />
+ <gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-2 gl-text-secondary" />
{{ childrenCountLabel }}
</span>
</div>
@@ -294,7 +297,7 @@ export default {
<template v-else>
<div v-if="isChildrenEmpty && !isShownAddForm && !error" data-testid="links-empty">
- <p class="gl-mt-3 gl-mb-4">
+ <p class="gl-mb-3">
{{ $options.i18n.emptyStateMessage }}
</p>
</div>
@@ -305,6 +308,7 @@ export default {
:issuable-gid="issuableGid"
:children-ids="childrenIds"
:parent-confidential="confidential"
+ :parent-iteration="issuableIteration"
@cancel="hideAddForm"
@addWorkItemChild="addChild"
/>
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 8b848995d44..a01f4616cab 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
@@ -16,7 +16,7 @@ export default {
GlFormGroup,
GlFormInput,
},
- inject: ['projectPath'],
+ inject: ['projectPath', 'hasIterationsFeature'],
props: {
issuableGid: {
type: String,
@@ -33,6 +33,11 @@ export default {
required: false,
default: false,
},
+ parentIteration: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
apollo: {
workItemTypes: {
@@ -77,6 +82,9 @@ export default {
taskWorkItemType() {
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
},
+ parentIterationId() {
+ return this.parentIteration?.id;
+ },
},
methods: {
getIdFromGraphQLId,
@@ -133,6 +141,13 @@ export default {
} else {
this.unsetError();
this.$emit('addWorkItemChild', data.workItemCreate.workItem);
+ /**
+ * call update mutation only when there is an iteration associated with the issue
+ */
+ // TODO: setting the iteration should be moved to the creation mutation once the backend is done
+ if (this.parentIterationId && this.hasIterationsFeature) {
+ this.addIterationToWorkItem(data.workItemCreate.workItem.id);
+ }
}
})
.catch(() => {
@@ -143,6 +158,19 @@ export default {
this.childToCreateTitle = null;
});
},
+ async addIterationToWorkItem(workItemId) {
+ await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: workItemId,
+ iterationWidget: {
+ iterationId: this.parentIterationId,
+ },
+ },
+ },
+ });
+ },
},
i18n: {
inputLabel: __('Title'),
@@ -182,7 +210,7 @@ export default {
>
<template #result="{ item }">
<div class="gl-display-flex">
- <div class="gl-text-gray-400 gl-mr-4">{{ getIdFromGraphQLId(item.id) }}</div>
+ <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(item.id) }}</div>
<div>{{ item.title }}</div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
new file mode 100644
index 00000000000..c4a36e36555
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -0,0 +1,248 @@
+<script>
+import {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSkeletonLoader,
+ GlSearchBoxByType,
+ GlDropdownText,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { debounce } from 'lodash';
+import Tracking from '~/tracking';
+import { s__ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ TRACKING_CATEGORY_SHOW,
+} from '../constants';
+
+const noMilestoneId = 'no-milestone-id';
+
+export default {
+ i18n: {
+ MILESTONE: s__('WorkItem|Milestone'),
+ NONE: s__('WorkItem|None'),
+ MILESTONE_PLACEHOLDER: s__('WorkItem|Add to milestone'),
+ NO_MATCHING_RESULTS: s__('WorkItem|No matching results'),
+ NO_MILESTONE: s__('WorkItem|No milestone'),
+ MILESTONE_FETCH_ERROR: s__(
+ 'WorkItem|Something went wrong while fetching milestones. Please try again.',
+ ),
+ },
+ components: {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSkeletonLoader,
+ GlSearchBoxByType,
+ GlDropdownText,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemMilestone: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ localMilestone: this.workItemMilestone,
+ searchTerm: '',
+ shouldFetch: false,
+ updateInProgress: false,
+ isFocused: false,
+ milestones: [],
+ };
+ },
+ computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_milestone',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ emptyPlaceholder() {
+ return this.canUpdate ? this.$options.i18n.MILESTONE_PLACEHOLDER : this.$options.i18n.NONE;
+ },
+ dropdownText() {
+ return this.localMilestone?.title || this.emptyPlaceholder;
+ },
+ isLoadingMilestones() {
+ return this.$apollo.queries.milestones.loading;
+ },
+ isNoMilestone() {
+ return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id;
+ },
+ dropdownClasses() {
+ return {
+ 'gl-text-gray-500!': this.canUpdate && this.isNoMilestone,
+ 'is-not-focused': !this.isFocused,
+ };
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ apollo: {
+ milestones: {
+ query: projectMilestonesQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ title: this.searchTerm,
+ first: 20,
+ };
+ },
+ skip() {
+ return !this.shouldFetch;
+ },
+ update(data) {
+ return data?.workspace?.attributes?.nodes || [];
+ },
+ error() {
+ this.$emit('error', this.i18n.MILESTONE_FETCH_ERROR);
+ },
+ },
+ },
+ methods: {
+ handleMilestoneClick(milestone) {
+ this.localMilestone = milestone;
+ },
+ onDropdownShown() {
+ this.$refs.search.focusInput();
+ this.shouldFetch = true;
+ this.isFocused = true;
+ },
+ onDropdownHide() {
+ this.isFocused = false;
+ this.searchTerm = '';
+ this.shouldFetch = false;
+ this.updateMilestone();
+ },
+ setSearchKey(value) {
+ this.searchTerm = value;
+ },
+ isMilestoneChecked(milestone) {
+ return this.localMilestone?.id === milestone?.id;
+ },
+ updateMilestone() {
+ if (this.workItemMilestone?.id === this.localMilestone?.id) {
+ return;
+ }
+
+ this.track('updated_milestone');
+ this.updateInProgress = true;
+ this.$apollo
+ .mutate({
+ mutation: localUpdateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ milestone: {
+ milestoneId: this.localMilestone?.id,
+ },
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.workItemUpdate.errors.length) {
+ throw new Error(data.workItemUpdate.errors.join('\n'));
+ }
+ })
+ .catch((error) => {
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+ this.$emit('error', msg);
+ Sentry.captureException(error);
+ })
+ .finally(() => {
+ this.updateInProgress = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ class="work-item-dropdown"
+ :label="$options.i18n.MILESTONE"
+ label-class="gl-pb-0! gl-overflow-wrap-break gl-mt-3"
+ label-cols="3"
+ label-cols-lg="2"
+ >
+ <span
+ v-if="!canUpdate"
+ class="gl-text-secondary gl-ml-4 gl-mt-3 gl-display-inline-block gl-line-height-normal"
+ data-testid="disabled-text"
+ >
+ {{ dropdownText }}
+ </span>
+ <gl-dropdown
+ v-else
+ :toggle-class="dropdownClasses"
+ :text="dropdownText"
+ :loading="updateInProgress"
+ @shown="onDropdownShown"
+ @hide="onDropdownHide"
+ >
+ <template #header>
+ <gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" />
+ </template>
+ <gl-dropdown-item
+ data-testid="no-milestone"
+ is-check-item
+ :is-checked="isNoMilestone"
+ @click="handleMilestoneClick({ id: 'no-milestone-id' })"
+ >
+ {{ $options.i18n.NO_MILESTONE }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-text v-if="isLoadingMilestones">
+ <gl-skeleton-loader :height="90">
+ <rect width="380" height="10" x="10" y="15" rx="4" />
+ <rect width="280" height="10" x="10" y="30" rx="4" />
+ <rect width="380" height="10" x="10" y="50" rx="4" />
+ <rect width="280" height="10" x="10" y="65" rx="4" />
+ </gl-skeleton-loader>
+ </gl-dropdown-text>
+ <template v-else-if="milestones.length">
+ <gl-dropdown-item
+ v-for="milestone in milestones"
+ :key="milestone.id"
+ is-check-item
+ :is-checked="isMilestoneChecked(milestone)"
+ @click="handleMilestoneClick(milestone)"
+ >
+ {{ milestone.title }}
+ </gl-dropdown-item>
+ </template>
+ <gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text>
+ </gl-dropdown>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
index 31e75663055..96a6493357c 100644
--- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue
+++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
@@ -53,7 +53,7 @@ export default {
v-gl-tooltip.hover="showTooltipOnHover"
:name="iconName"
:title="workItemTooltipTitle"
- class="gl-mr-2 gl-text-gray-500"
+ class="gl-mr-2 gl-text-secondary"
/>
<span v-if="workItemTypeName" :class="{ 'gl-sr-only': !showText }">{{ workItemTypeName }}</span>
</span>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 78219e62d01..7737c535650 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -17,6 +17,9 @@ export const WIDGET_TYPE_LABELS = 'LABELS';
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
+export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
+export const WIDGET_TYPE_ITERATION = 'ITERATION';
+
export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
@@ -26,13 +29,19 @@ export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE';
export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const i18n = {
- fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'),
+ fetchErrorTitle: s__('WorkItem|Work item not found'),
+ fetchError: s__(
+ "WorkItem|This work item is not available. It either doesn't exist or you don't have permission to view it.",
+ ),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
confidentialTooltip: s__(
'WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this task.',
),
};
+export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(
+ 'WorkItem|Something went wrong when fetching labels. Please try again.',
+);
export const I18N_WORK_ITEM_ERROR_CREATING = s__(
'WorkItem|Something went wrong when creating %{workItemType}. Please try again.',
);
@@ -48,6 +57,10 @@ export const I18N_WORK_ITEM_ARE_YOU_SURE_DELETE = s__(
);
export const I18N_WORK_ITEM_DELETED = s__('WorkItem|%{workItemType} deleted');
+export const I18N_WORK_ITEM_FETCH_ITERATIONS_ERROR = s__(
+ 'WorkItem|Something went wrong when fetching iterations. Please try again.',
+);
+
export const sprintfWorkItem = (msg, workItemTypeArg) => {
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
return capitalizeFirstCharacter(
diff --git a/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
new file mode 100644
index 00000000000..6edb6c89f16
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
@@ -0,0 +1,9 @@
+query issuableDetails($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ issuable: issue(iid: $iid) {
+ id
+ confidential
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index 36ffba8a540..36779dfe11e 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -1,6 +1,6 @@
enum LocalWidgetType {
ASSIGNEES
- LABELS
+ MILESTONE
}
interface LocalWorkItemWidget {
@@ -12,10 +12,9 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget {
nodes: [UserCore]
}
-type LocalWorkItemLabels implements LocalWorkItemWidget {
+type LocalWorkItemMilestone implements LocalWorkItemWidget {
type: LocalWidgetType!
- allowScopedLabels: Boolean!
- nodes: [Label!]
+ nodes: [Milestone!]
}
extend type WorkItem {
@@ -30,17 +29,14 @@ input LocalUserInput {
avatarUrl: String
}
-input LocalLabelInput {
- id: ID!
- title: String!
- color: String
- description: String
+input LocalMilestoneInput {
+ milestoneId: ID!
}
input LocalUpdateWorkItemInput {
id: WorkItemID!
assignees: [LocalUserInput!]
- labels: [LocalLabelInput]
+ milestone: LocalMilestoneInput!
}
type LocalWorkItemPayload {
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 f4c77ed2ec0..bb05c9b2135 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -1,4 +1,3 @@
-#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "ee_else_ce/work_items/graphql/work_item_widgets.fragment.graphql"
fragment WorkItem on WorkItem {
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
index 276061af193..fa0ab56df75 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -1,15 +1,16 @@
-#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "./work_item.fragment.graphql"
query workItem($id: WorkItemID!) {
workItem(id: $id) {
...WorkItem
mockWidgets @client {
- ... on LocalWorkItemLabels {
+ ... on LocalWorkItemMilestone {
type
- allowScopedLabels
nodes {
- ...Label
+ id
+ title
+ expired
+ dueDate
}
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql
new file mode 100644
index 00000000000..d5b2de8c4c6
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql
@@ -0,0 +1,21 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+subscription issuableAssignees($issuableId: IssuableID!) {
+ issuableAssigneesUpdated(issuableId: $issuableId) {
+ ... on WorkItem {
+ id
+ widgets {
+ ... on WorkItemWidgetAssignees {
+ type
+ allowsMultipleAssignees
+ canInviteMembers
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql
index 7e045fdf431..d8760f147e1 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql
@@ -4,6 +4,7 @@ subscription issuableDatesUpdated($issuableId: IssuableID!) {
id
widgets {
... on WorkItemWidgetStartAndDueDate {
+ type
dueDate
startDate
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql
new file mode 100644
index 00000000000..86d936bf4dd
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql
@@ -0,0 +1,19 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
+subscription workItemLabels($issuableId: IssuableID!) {
+ issuableLabelsUpdated(issuableId: $issuableId) {
+ ... on WorkItem {
+ id
+ widgets {
+ ... on WorkItemWidgetLabels {
+ type
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
index 3005069f59a..d404cfb10ed 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
@@ -1,8 +1,16 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetDescription {
type
description
descriptionHtml
+ lastEditedAt
+ lastEditedBy {
+ name
+ webPath
+ }
}
... on WorkItemWidgetAssignees {
type
@@ -14,6 +22,14 @@ fragment WorkItemWidgets on WorkItemWidget {
}
}
}
+ ... on WorkItemWidgetLabels {
+ type
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
... on WorkItemWidgetStartAndDueDate {
type
dueDate
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index bb4c7052238..f872d8c6b12 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -6,7 +6,13 @@ import { createRouter } from './router';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
- const { fullPath, hasIssueWeightsFeature, issuesListPath } = el.dataset;
+ const {
+ fullPath,
+ hasIssueWeightsFeature,
+ issuesListPath,
+ projectNamespace,
+ hasIterationsFeature,
+ } = el.dataset;
return new Vue({
el,
@@ -17,6 +23,8 @@ export const initWorkItemsRoot = () => {
fullPath,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
issuesListPath,
+ projectNamespace,
+ hasIterationsFeature: parseBoolean(hasIterationsFeature),
},
render(createElement) {
return createElement(App);
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index 3b7257591e2..4908b99e5b0 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -6,7 +6,6 @@ import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
-import createWorkItemFromTaskMutation from '../graphql/create_work_item_from_task.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import ItemTitle from '../components/item_title.vue';
@@ -29,26 +28,6 @@ export default {
required: false,
default: '',
},
- issueGid: {
- type: String,
- required: false,
- default: '',
- },
- lockVersion: {
- type: Number,
- required: false,
- default: null,
- },
- lineNumberStart: {
- type: String,
- required: false,
- default: null,
- },
- lineNumberEnd: {
- type: String,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -136,28 +115,6 @@ export default {
this.error = this.createErrorText;
}
},
- async createWorkItemFromTask() {
- try {
- const { data } = await this.$apollo.mutate({
- mutation: createWorkItemFromTaskMutation,
- variables: {
- input: {
- id: this.issueGid,
- workItemData: {
- lockVersion: this.lockVersion,
- title: this.title,
- lineNumberStart: Number(this.lineNumberStart),
- lineNumberEnd: Number(this.lineNumberEnd),
- workItemTypeId: this.selectedWorkItemType,
- },
- },
- },
- });
- this.$emit('onCreate', data.workItemCreateFromTask.workItem.descriptionHtml);
- } catch {
- this.error = this.createErrorText;
- }
- },
handleTitleInput(title) {
this.title = title;
},
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 9e81e1d4771..21d9db26382 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -1,13 +1,9 @@
@import './pages/branches';
-@import './pages/clusters';
@import './pages/colors';
@import './pages/commits';
-@import './pages/deploy_keys';
@import './pages/detail_page';
-@import './pages/environment_logs';
@import './pages/events';
@import './pages/groups';
-@import './pages/help';
@import './pages/hierarchy';
@import './pages/issuable';
@import './pages/issues';
@@ -21,11 +17,8 @@
@import './pages/pipelines';
@import './pages/profile';
@import './pages/projects';
-@import './pages/prometheus';
@import './pages/registry';
@import './pages/search';
-@import './pages/service_desk';
@import './pages/settings';
@import './pages/storage_quota';
-@import './pages/tree';
@import './pages/users';
diff --git a/app/assets/stylesheets/bootstrap_migration_reset.scss b/app/assets/stylesheets/bootstrap_migration_reset.scss
index ad315c4ada1..fb112a2ee84 100644
--- a/app/assets/stylesheets/bootstrap_migration_reset.scss
+++ b/app/assets/stylesheets/bootstrap_migration_reset.scss
@@ -54,10 +54,6 @@ strong {
font-weight: bold;
}
-a {
- color: $blue-600;
-}
-
hr {
overflow: hidden;
}
diff --git a/app/assets/stylesheets/components/batch_comments/review_bar.scss b/app/assets/stylesheets/components/batch_comments/review_bar.scss
deleted file mode 100644
index 5e1128dc4ce..00000000000
--- a/app/assets/stylesheets/components/batch_comments/review_bar.scss
+++ /dev/null
@@ -1,71 +0,0 @@
-.review-bar-component {
- position: fixed;
- bottom: 0;
- left: 0;
- z-index: $zindex-dropdown-menu;
- display: flex;
- align-items: center;
- width: 100%;
- height: $toggle-sidebar-height;
- padding-left: $contextual-sidebar-width;
- padding-right: $gutter_collapsed_width;
- background: $white;
- border-top: 1px solid $border-color;
- transition: padding $gl-transition-duration-medium;
-
- .page-with-icon-sidebar & {
- padding-left: $contextual-sidebar-collapsed-width;
- }
-
- .right-sidebar-expanded & {
- padding-right: $gutter_width;
- }
-
- @media (max-width: map-get($grid-breakpoints, sm)-1) {
- padding-left: 0;
- padding-right: 0;
- }
-
- .dropdown {
- margin-left: $grid-size;
- }
-}
-
-.review-bar-content {
- max-width: $limited-layout-width;
- padding: 0 $gl-padding;
- width: 100%;
- margin: 0 auto;
-}
-
-.review-preview-item-header {
- display: flex;
- align-items: center;
- width: 100%;
- margin-bottom: 4px;
-
- > .bold {
- display: flex;
- min-width: 0;
- line-height: 16px;
- }
-}
-
-.review-preview-item-footer {
- display: flex;
- align-items: center;
- margin-top: 4px;
-}
-
-.review-preview-item-content {
- width: 100%;
-
- p {
- display: block;
- width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- margin-bottom: 0;
- }
-}
diff --git a/app/assets/stylesheets/components/date_time_picker.scss b/app/assets/stylesheets/components/date_time_picker.scss
deleted file mode 100644
index 21f085cdaf1..00000000000
--- a/app/assets/stylesheets/components/date_time_picker.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.date-time-picker {
- .date-time-picker-menu {
- width: 400px;
- }
-}
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
deleted file mode 100644
index b8bd1000bfd..00000000000
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ /dev/null
@@ -1,193 +0,0 @@
-$design-pin-diameter: 28px;
-$design-pin-diameter-sm: 24px;
-$t-gray-a-16-design-pin: rgba($black, 0.16);
-
-.layout-page.design-detail-layout {
- max-height: 100vh;
-}
-
-.design-detail {
- background-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity);
-
- .with-performance-bar & {
- top: 35px;
- }
-
- .comment-indicator {
- border-radius: 50%;
- }
-
- .comment-indicator,
- .frame .design-note-pin {
- &:active {
- cursor: grabbing;
- }
- }
-}
-
-.design-scaler-wrapper {
- bottom: 0;
- left: 50%;
- transform: translateX(-50%);
-}
-
-.design-checkbox {
- position: absolute;
- top: $gl-padding;
- left: 30px;
-}
-
-.image-notes {
- overflow-y: scroll;
- padding: $gl-padding;
- padding-top: 50px;
- background-color: $white;
- flex-shrink: 0;
- min-width: 400px;
- flex-basis: 28%;
-
- .link-inherit-color {
- &:hover,
- &:active,
- &:focus {
- color: inherit;
- text-decoration: none;
- }
- }
-
- .toggle-comments {
- line-height: 20px;
- border-top: 1px solid $border-color;
-
- &.expanded {
- border-bottom: 1px solid $border-color;
- }
-
- .toggle-comments-button:focus {
- text-decoration: none;
- color: $blue-600;
- }
- }
-
- .design-note-pin {
- margin-left: $gl-padding;
- }
-
- .design-discussion {
- margin: $gl-padding 0;
-
- &::before {
- content: '';
- border-left: 1px solid $gray-100;
- position: absolute;
- left: 28px;
- top: -17px;
- height: 17px;
- }
-
- .design-note {
- padding: $gl-padding;
- list-style: none;
- transition: background $gl-transition-duration-medium $general-hover-transition-curve;
- border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box
- border-top-right-radius: $border-radius-default;
-
- a {
- color: inherit;
- }
-
- .note-text a {
- color: $blue-600;
- }
- }
-
- .reply-wrapper {
- padding: $gl-padding;
- }
- }
-
- .reply-wrapper {
- border-top: 1px solid $border-color;
- }
-
- .new-discussion-disclaimer {
- line-height: 20px;
- }
-}
-
-@media (max-width: map-get($grid-breakpoints, lg)) {
- .design-detail {
- overflow-y: scroll;
- }
-
- .image-notes {
- overflow-y: auto;
- min-width: 100%;
- flex-grow: 1;
- flex-basis: auto;
- }
-}
-
-.design-card-header {
- background: transparent;
-}
-
-.design-note-pin {
- display: flex;
- height: $design-pin-diameter;
- width: $design-pin-diameter;
- box-sizing: content-box;
- background-color: $purple-500;
- color: $white;
- font-weight: $gl-font-weight-bold;
- border-radius: 50%;
- z-index: 1;
- padding: 0;
- border: 0;
-
- &.draft {
- background-color: $orange-500;
- }
-
- &.resolved {
- background-color: $gray-500;
- }
-
- &.on-image {
- box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24;
- border: $white 2px solid;
- will-change: transform, box-shadow, opacity;
- // NOTE: verbose transition property required for Safari
- transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear;
- transform-origin: 0 0;
- transform: translate(-50%, -50%);
-
- &:hover {
- transform: scale(1.2) translate(-50%, -50%);
- }
-
- &:active {
- box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin;
- }
-
- &.inactive {
- @include gl-opacity-5;
-
- &:hover {
- @include gl-opacity-10;
- }
- }
- }
-
- &.small {
- position: absolute;
- border: 1px solid $white;
- height: $design-pin-diameter-sm;
- width: $design-pin-diameter-sm;
- }
-
- &.user-avatar {
- top: 25px;
- right: 8px;
- }
-}
diff --git a/app/assets/stylesheets/components/design_management/design_list_item.scss b/app/assets/stylesheets/components/design_management/design_list_item.scss
deleted file mode 100644
index 09af4da37e9..00000000000
--- a/app/assets/stylesheets/components/design_management/design_list_item.scss
+++ /dev/null
@@ -1,19 +0,0 @@
-.design-list-item {
- height: 280px;
- text-decoration: none;
-
- .icon-version-status {
- position: absolute;
- right: 10px;
- top: 10px;
- }
-
- .card-body {
- height: 230px;
- }
-}
-
-// This is temporary class to be removed after feature flag removal: https://gitlab.com/gitlab-org/gitlab/-/issues/223197
-.design-list-item-new {
- height: 210px;
-}
diff --git a/app/assets/stylesheets/components/feature_highlight.scss b/app/assets/stylesheets/components/feature_highlight.scss
deleted file mode 100644
index 4d301cc5617..00000000000
--- a/app/assets/stylesheets/components/feature_highlight.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.gl-sm-mr-3 {
- @media (min-width: $breakpoint-sm) {
- @include gl-mr-3;
- }
-}
diff --git a/app/assets/stylesheets/components/milestone_combobox.scss b/app/assets/stylesheets/components/milestone_combobox.scss
deleted file mode 100644
index 94d295c324b..00000000000
--- a/app/assets/stylesheets/components/milestone_combobox.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.milestone-combobox {
- .dropdown-menu.show {
- overflow: hidden;
- }
-}
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index 3bb889b6ba0..293caf6fc87 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -75,19 +75,6 @@ $item-remove-button-space: 42px;
}
}
-.item-body,
-.card-header {
- .health-label-short {
- max-width: 0;
- }
-}
-
-.card-header {
- .health-label-short {
- display: initial;
- }
-}
-
.item-meta {
flex-basis: 100%;
font-size: $gl-font-size;
@@ -212,11 +199,6 @@ $item-remove-button-space: 42px;
max-width: 90%;
}
- .card-header {
- .health-label-short {
- max-width: 30px;
- }
- }
}
/* Small devices (landscape phones, 768px and up) */
@@ -239,11 +221,6 @@ $item-remove-button-space: 42px;
}
}
- .card-header {
- .health-label-short {
- max-width: 60px;
- }
- }
}
/* Medium devices (desktops, 992px and up) */
@@ -257,12 +234,6 @@ $item-remove-button-space: 42px;
font-size: inherit; // Base size given to `item-meta` is `$gl-font-size-small`
}
}
-
- .card-header {
- .health-label-short {
- max-width: 100px;
- }
- }
}
/* Large devices (large desktops, 1200px and up) */
@@ -309,15 +280,3 @@ $item-remove-button-space: 42px;
flex-basis: auto;
}
}
-
-@media only screen and (min-width: 1500px) {
- .card-header {
- .health-label-short {
- display: none;
- }
-
- .health-label-long {
- display: block;
- }
- }
-}
diff --git a/app/assets/stylesheets/components/release_block.scss b/app/assets/stylesheets/components/release_block.scss
deleted file mode 100644
index 7e82d0960d7..00000000000
--- a/app/assets/stylesheets/components/release_block.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.release-block {
- transition: background-color 1s linear;
-}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/components/shortcuts_help.scss
index 9182292ffd3..ea2281538b4 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/components/shortcuts_help.scss
@@ -27,14 +27,3 @@
}
}
}
-
-.documentation {
- padding: 7px;
- font-size: $gl-font-size-large;
-}
-
-.card.links-card {
- a {
- color: $blue-600;
- }
-}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index e977fb92928..07db6b3c147 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -42,7 +42,6 @@
@import 'framework/notes';
@import 'framework/tabs';
@import 'framework/timeline';
-@import 'framework/toggle';
@import 'framework/typography';
@import 'framework/zen';
@import 'framework/wells';
@@ -54,14 +53,11 @@
@import 'framework/emojis';
@import 'framework/icons';
@import 'framework/snippets';
-@import 'framework/memory_graph';
@import 'framework/responsive_tables';
@import 'framework/stacked_progress_bar';
@import 'framework/sortable';
-@import 'framework/ci_variable_list';
@import 'framework/feature_highlight';
@import 'framework/read_more';
-@import 'framework/flex_grid';
@import 'framework/system_messages';
@import 'framework/spinner';
@import 'framework/card';
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index f947042ba51..799777977ed 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -66,7 +66,6 @@
}
&.content-component-block {
- padding: 8px 0;
background-color: $body-bg;
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index b1e5ca50a8b..e69d7b4462d 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -1,9 +1,17 @@
.user-contrib-cell {
+ stroke: $t-gray-a-08;
+
&:hover {
cursor: pointer;
stroke: $black;
}
+ &:focus {
+ @include gl-outline-none;
+ stroke: $white;
+ filter: drop-shadow(1px 0 0.5px $blue-400) drop-shadow(0 1px 0.5px $blue-400) drop-shadow(-1px 0 0.5px $blue-400) drop-shadow(0 -1px 0.5px $blue-400);
+ }
+
// `app/assets/javascripts/pages/users/activity_calendar.js` sets this attribute
@for $i from 1 through length($calendar-activity-colors) {
$color: nth($calendar-activity-colors, $i);
diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss
deleted file mode 100644
index ef4355ad157..00000000000
--- a/app/assets/stylesheets/framework/ci_variable_list.scss
+++ /dev/null
@@ -1,77 +0,0 @@
-.ci-variable-list {
- margin-left: 0;
- margin-bottom: 0;
- padding-left: 0;
- list-style: none;
- clear: both;
-}
-
-.ci-variable-row {
- display: flex;
- align-items: flex-start;
-
- @include media-breakpoint-down(xs) {
- align-items: flex-end;
- }
-
- &:not(:last-child) {
- margin-bottom: $gl-btn-padding;
-
- @include media-breakpoint-down(xs) {
- margin-bottom: 3 * $gl-btn-padding;
- }
- }
-
- &:last-child {
- .ci-variable-body-item:last-child {
- margin-right: $ci-variable-remove-button-width;
-
- @include media-breakpoint-down(xs) {
- margin-right: 0;
- }
- }
-
- .ci-variable-row-remove-button {
- display: none;
- }
-
- @include media-breakpoint-down(xs) {
- .ci-variable-row-body {
- margin-right: $ci-variable-remove-button-width;
- }
- }
- }
-}
-
-.ci-variable-row-body {
- display: flex;
- align-items: flex-start;
- width: 100%;
- padding-bottom: $gl-padding;
-
- @include media-breakpoint-down(xs) {
- display: block;
- }
-}
-
-.ci-variable-body-item {
- flex: 1;
-
- &:not(:last-child) {
- margin-right: $gl-btn-padding;
-
- @include media-breakpoint-down(xs) {
- margin-right: 0;
- margin-bottom: $gl-btn-padding;
- }
- }
-}
-
-.ci-variable-masked-item,
-.ci-variable-protected-item {
- flex: 0 1 auto;
- display: flex;
- align-items: center;
- padding-top: 5px;
- padding-bottom: 5px;
-}
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 8d1fb5eb55f..f7cd5d7e183 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -43,7 +43,7 @@
z-index: 120;
&.is-sidebar-moved {
- --initial-top: calc(#{$header-height} + #{$mr-tabs-height + 28px});
+ --initial-top: calc(#{$header-height} + #{$mr-tabs-height + 24px});
}
.with-system-header & {
@@ -578,78 +578,6 @@ table.code {
}
}
-// Merge request diff grid layout
-.diff-grid {
- .diff-td {
- // By default min-width is auto with 1fr which causes some overflow problems
- // https://gitlab.com/gitlab-org/gitlab/-/issues/296222
- min-width: 0;
- }
-
- .diff-grid-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
-
- &.diff-grid-row-full {
- grid-template-columns: 1fr;
- }
- }
-
- .diff-grid-left,
- .diff-grid-right {
- display: grid;
- // Zero width column is a placeholder for the EE inline code quality diff
- // see ee/.../diffs.scss for more details
- grid-template-columns: 50px 8px 0 1fr;
- }
-
- .diff-grid-2-col {
- grid-template-columns: 100px 1fr !important;
-
- &.parallel {
- grid-template-columns: 50px 1fr !important;
- }
- }
-
- .diff-grid-comments {
- display: grid;
- grid-template-columns: 1fr 1fr;
- }
-
- .diff-grid-drafts {
- display: grid;
- grid-template-columns: 1fr 1fr;
- }
-
- &.inline-diff-view {
- .diff-grid-comments {
- display: grid;
- grid-template-columns: 1fr;
- }
-
- .diff-grid-drafts {
- display: grid;
- grid-template-columns: 1fr;
- }
-
- .diff-grid-row {
- grid-template-columns: 1fr;
- }
-
- .diff-grid-left,
- .diff-grid-right {
- // Zero width column is a placeholder for the EE inline code quality diff
- // see ee/../diffs.scss for more details
- grid-template-columns: 50px 50px 8px 0 1fr;
- }
- }
-}
-
-// Merge request diff grid layout overrides
-.diff-table.code .diff-tr.line_holder .diff-td.line_content.parallel {
- width: unset;
-}
-
.diff-stats {
align-items: center;
padding: 0 1rem;
@@ -730,68 +658,6 @@ table.code {
}
}
-.diff-comment-avatar-holders {
- position: absolute;
- margin-left: -$gl-padding;
- z-index: 100;
- @include code-icon-size();
-
- &:hover {
- .diff-comment-avatar,
- .diff-comments-more-count {
- @for $i from 1 through 4 {
- $x-pos: 14px;
-
- &:nth-child(#{$i}) {
- @if $i == 4 {
- $x-pos: 14.5px;
- }
-
- transform: translateX((($i * $x-pos) - $x-pos));
-
- &:hover {
- transform: translateX((($i * $x-pos) - $x-pos));
- }
- }
- }
- }
-
- .diff-comments-more-count {
- padding-left: 2px;
- padding-right: 2px;
- width: auto;
- }
- }
-}
-
-.diff-comment-avatar,
-.diff-comments-more-count {
- position: absolute;
- left: 0;
- margin-right: 0;
- border-color: $white;
- cursor: pointer;
- transition: all 0.1s ease-out;
- @include code-icon-size();
-
- @for $i from 1 through 4 {
- &:nth-child(#{$i}) {
- z-index: (4 - $i);
- }
- }
-
- .avatar {
- @include code-icon-size();
- }
-}
-
-.diff-comments-more-count {
- padding-left: 0;
- padding-right: 0;
- overflow: hidden;
- @include code-icon-size();
-}
-
.diff-comments-more-count,
.diff-notes-collapse,
.diff-codequality-collapse {
@@ -867,70 +733,6 @@ table.code {
}
}
-
-.diff-file-changes {
- max-width: 560px;
- width: 100%;
- z-index: 150;
- min-height: $dropdown-min-height;
- max-height: $dropdown-max-height;
- overflow-y: auto;
- margin-bottom: 0;
-
- @include media-breakpoint-up(sm) {
- left: $gl-padding;
- }
-
- .dropdown-input .dropdown-input-search {
- pointer-events: all;
- }
-
- .diff-changed-file {
- display: flex;
- padding-top: 8px;
- padding-bottom: 8px;
- min-width: 0;
- }
-
- .diff-file-changed-icon {
- margin-top: 2px;
- }
-
- .diff-changed-file-content {
- display: flex;
- flex-direction: column;
- min-width: 0;
- }
-
- .diff-changed-file-name,
- .diff-changed-blank-file-name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- .diff-changed-blank-file-name {
- color: $gl-text-color-tertiary;
- font-style: italic;
- }
-
- .diff-changed-file-path {
- color: $gl-text-color-tertiary;
- }
-
- .diff-changed-stats {
- margin-left: auto;
- white-space: nowrap;
- }
-}
-
-.diff-file-changes-path {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
.note-container {
background-color: $gray-light;
border-top: 1px solid $white-normal;
@@ -1007,27 +809,6 @@ table.code {
}
}
-// Notes tweaks for the Changes tab ONLY
-.diff-tr {
- .timeline-discussion-body {
- clear: left;
-
- .note-body {
- margin-top: 0 !important;
- }
- }
-
- .timeline-entry img.avatar {
- margin-top: -2px;
- margin-right: $gl-padding-8;
- }
-
- // tiny adjustment to vertical align with the note header text
- .discussion-collapsible .timeline-icon {
- padding-top: 2px;
- }
-}
-
.files:not([data-can-create-note]) .frame {
cursor: auto;
}
@@ -1097,6 +878,7 @@ table.code {
.discussion-notes {
min-height: 35px;
+ background-color: transparent;
&:first-child {
// First child does not have the jagged borders
@@ -1121,6 +903,17 @@ table.code {
display: none;
}
}
+
+ ul.notes {
+ li.toggle-replies-widget,
+ .discussion-reply-holder {
+ margin-left: 2.5rem;
+
+ .reply-author-avatar {
+ height: 1.5rem;
+ }
+ }
+ }
}
.discussion-body .image .frame {
@@ -1183,9 +976,15 @@ table.code {
bottom: 100vh;
}
-.diff-line-expand-button {
- &:hover,
- &:focus {
- @include gl-bg-gray-200;
+.diff-grid-row.expansion.match {
+ border-top: 1px solid var(--diff-expansion-background-color);
+ border-bottom: 1px solid var(--diff-expansion-background-color);
+
+ &:first-child {
+ border-top: 0;
+ }
+
+ &:last-child {
+ border-bottom: 0;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d91524d99e6..d561a7d9450 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -469,6 +469,7 @@
.sidebar-participant {
.merge-icon {
top: calc(50% + 5px);
+ left: 3rem;
}
}
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index b51daf0e4dc..b63365e8159 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -46,12 +46,14 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
.flash-notice,
.flash-success,
.flash-warning {
- padding: $gl-padding $gl-padding-32 $gl-padding ($gl-padding + $gl-padding-4);
- margin-top: 10px;
-
- .container-fluid,
- .container-fluid.container-limited {
- background: transparent;
+ &:not(.gl-alert) {
+ padding: $gl-padding $gl-padding-32 $gl-padding ($gl-padding + $gl-padding-4);
+ margin-top: 10px;
+
+ .container-fluid,
+ .container-fluid.container-limited {
+ background: transparent;
+ }
}
}
@@ -79,6 +81,19 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
.gl-alert {
@include gl-my-4;
}
+
+ &.flash-container-no-margin {
+ .gl-alert {
+ @include gl-my-0;
+ }
+
+ .flash-alert,
+ .flash-notice,
+ .flash-success,
+ .flash-warning {
+ @include gl-mt-0;
+ }
+ }
}
@include media-breakpoint-down(sm) {
diff --git a/app/assets/stylesheets/framework/flex_grid.scss b/app/assets/stylesheets/framework/flex_grid.scss
deleted file mode 100644
index 10537fd5549..00000000000
--- a/app/assets/stylesheets/framework/flex_grid.scss
+++ /dev/null
@@ -1,52 +0,0 @@
-.flex-grid {
- .grid-row {
- border-bottom: 1px solid $border-color;
- padding: 0;
-
- &:last-child {
- border-bottom: 0;
- }
-
- @include media-breakpoint-down(md) {
- border-bottom: 0;
- border-right: 1px solid $border-color;
-
- &:last-child {
- border-right: 0;
- }
- }
-
- @include media-breakpoint-down(xs) {
- border-right: 0;
- border-bottom: 1px solid $border-color;
-
- &:last-child {
- border-bottom: 0;
- }
- }
- }
-
- .grid-cell {
- padding: 10px $gl-padding;
- border-right: 1px solid $border-color;
-
- &:last-child {
- border-right: 0;
- }
-
- @include media-breakpoint-up(md) {
- flex: 1;
- }
-
- @include media-breakpoint-down(md) {
- border-right: 0;
- flex: none;
- }
- }
-}
-
-.card {
- .card-body.flex-grid {
- padding: 0;
- }
-}
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index 40e11b50eba..66d163f608a 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -8,13 +8,15 @@
font-size: 95%;
}
-.gfm-project_member {
+.gfm-project_member,
+.md a.gfm-project_member {
padding: 0 2px;
background-color: $blue-100;
border-radius: $border-radius-default;
+ color: $blue-700;
&.current-user {
- background-color: $orange-50;
+ background-color: $orange-100;
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index d2bb1e3d555..e9a507ebb6b 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -109,12 +109,6 @@ ul.content-list {
color: $gl-text-color;
word-break: break-word;
- &.no-description {
- .title {
- line-height: $list-text-height;
- }
- }
-
.title {
font-weight: $gl-font-weight-bold;
}
@@ -221,6 +215,7 @@ ul.content-list {
}
}
+ul.content-list.content-list-items-padding > li,
ul.content-list.issuable-list > li,
ul.content-list.todos-list > li,
.card > .content-list > li {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index b623f18c4ae..c40cadafb9c 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -44,12 +44,6 @@
}
}
-.div-dropzone-alert {
- margin-top: 5px;
- margin-bottom: 0;
- transition: opacity 200ms ease-in-out;
-}
-
.md-header {
.nav-links {
a {
@@ -155,8 +149,16 @@
.md-suggestion-diff {
display: table !important;
border: 1px solid $border-color !important;
- width: 100% !important;
- font-family: $monospace-font !important;
+
+ td {
+ border: 0 !important;
+ }
+
+ tr.old {
+ td {
+ border-radius: 0 !important;
+ }
+ }
}
.suggestions.md > .markdown-code-block {
@@ -164,23 +166,12 @@
}
.md-suggestion-header {
- height: $suggestion-header-height;
display: flex;
align-items: center;
justify-content: space-between;
background-color: $gray-light;
border: 1px solid $border-color;
- padding: $gl-padding;
border-radius: $border-radius-default $border-radius-default 0 0;
-
- svg {
- vertical-align: middle;
- margin-bottom: 3px;
- }
-
- .dropdown-chevron {
- margin-bottom: 0;
- }
}
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/framework/memory_graph.scss b/app/assets/stylesheets/framework/memory_graph.scss
deleted file mode 100644
index 510969e149a..00000000000
--- a/app/assets/stylesheets/framework/memory_graph.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.memory-graph-container {
- background: $white;
- border: 1px solid $gray-100;
-}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index f39d53c5b1c..8b2a494527b 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -4,22 +4,6 @@
}
table {
- /*
- * TODO
- * This is a temporary workaround until we fix the neutral
- * color palette in https://gitlab.com/gitlab-org/gitlab/-/issues/213570
- *
- * The overwrites here affected the following areas:
- * - The subscription seats table. When removing this code, the .seats-table
- * <th> and margin overrides should be removed there.
- *
- * Remove this code as soon as this happens
- *
- */
- &.gl-table {
- @include gl-text-gray-500;
- }
-
&.table {
.thead-white {
th {
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 43effbdd7d7..32e9bba8712 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -28,15 +28,9 @@
.timeline-entry {
color: $gl-text-color;
- // [dark-theme]: only give background color to actual notes
- // in the timeline, the note form textarea has a background
- // of it's own
- &:not(.note-form) {
- background-color: $white;
- }
-
- &:not(.note-form).internal-note {
- background-color: $orange-50;
+ &:not(.note-form).internal-note .timeline-content,
+ &:not(.note-form).draft-note .timeline-content {
+ background-color: $orange-50 !important;
}
.timeline-entry-inner {
@@ -45,23 +39,15 @@
&:target,
&.target {
- background: $line-target-blue;
+ .timeline-content {
+ background: $line-target-blue !important;
+ }
&.system-note .note-body .note-text.system-note-commit-list::after {
background: linear-gradient(rgba($line-target-blue, 0.1) -100px, $line-target-blue 100%);
}
}
- img.avatar {
- margin-right: $gl-padding-12;
-
- @include media-breakpoint-down(sm) {
- width: $gl-spacing-scale-6;
- height: $gl-spacing-scale-6;
- margin-right: $gl-padding-8;
- }
- }
-
.controls {
padding-top: 10px;
float: right;
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
deleted file mode 100644
index fd888fdec65..00000000000
--- a/app/assets/stylesheets/framework/toggle.scss
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
-* Toggle button
-*
-* @usage
-* ### Active and Inactive text should be provided as data attributes:
-* <button type="button" class="project-feature-toggle" data-enabled-text="Enabled" data-disabled-text="Disabled">
-* <span class="gl-spinner loading-icon hidden" aria-label="Loading"></span>
-* </button>
-
-* ### Checked should have `is-checked` class
-* <button type="button" class="project-feature-toggle is-checked" data-enabled-text="Enabled" data-disabled-text="Disabled">
-* <span class="gl-spinner loading-icon hidden" aria-label="Loading"></span>
-* </button>
-
-* ### Disabled should have `is-disabled` class
-* <button type="button" class="project-feature-toggle is-disabled" data-enabled-text="Enabled" data-disabled-text="Disabled" disabled="true">
-* <span class="gl-spinner loading-icon hidden" aria-label="Loading"></span>
-* </button>
-
-* ### Loading should have `is-loading` and an icon with `loading-icon` class
-* <button type="button" class="project-feature-toggle is-loading" data-enabled-text="Enabled" data-disabled-text="Disabled">
-* <span class="gl-spinner loading-icon" aria-label="Loading"></span>
-* </button>
-*/
-.project-feature-toggle {
- position: relative;
- border: 0;
- outline: 0;
- display: block;
- width: 50px;
- height: 24px;
- cursor: pointer;
- user-select: none;
- background: $gray-400;
- border-radius: 12px;
- padding: 3px;
- transition: all 0.4s ease;
-
- &::selection,
- &::before::selection,
- &::after::selection {
- background: none;
- }
-
- &:focus {
- outline: none;
- }
-
- .toggle-icon {
- position: relative;
- display: block;
- left: 0;
- border-radius: 9px;
- background: $white;
- transition: all 0.2s ease;
- width: $default-icon-size;
- height: $default-icon-size;
- }
-
- .loading-icon {
- display: none;
- font-size: 12px;
- color: $white;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
-
- &.is-loading {
- .loading-icon {
- display: block;
-
- &::before {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
- }
- }
-
- &.is-checked {
- background: $blue-400;
-
- .toggle-icon {
- left: calc(100% - 18px);
- }
- }
-
- &.is-checked .toggle-icon .toggle-status-checked,
- .toggle-icon .toggle-status-unchecked {
- display: inline;
- }
-
- &.is-checked .toggle-icon .toggle-status-unchecked,
- &.is-loading .toggle-icon,
- .toggle-icon .toggle-status-checked {
- display: none;
- }
-
- &.is-disabled {
- opacity: 0.4;
- cursor: not-allowed;
- }
-
- @include media-breakpoint-down(xs) {
- width: 50px;
-
- &::before,
- &.is-checked::before {
- display: none;
- }
- }
-
- @keyframes animate-enabled {
- 0%,
-
- 35% { opacity: 0; }
-
- 100% { opacity: 1; }
- }
-
- @keyframes animate-disabled {
- 0%,
-
- 35% { opacity: 0; }
-
- 100% { opacity: 1; }
- }
-}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index e79fb843967..2c2d8a2b592 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -33,14 +33,6 @@
}
}
- a {
- color: $blue-600;
-
- > code {
- color: $blue-600;
- }
- }
-
.media-container {
display: inline-flex;
flex-direction: column;
@@ -717,10 +709,6 @@ textarea.js-gfm-input {
font-size: $gl-font-size-monospace;
}
-.strikethrough {
- text-decoration: line-through;
-}
-
h1,
h2,
h3,
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index bd32a817d5d..9cfc5a0201e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -382,6 +382,8 @@ $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: $white;
$gl-text-color-secondary-inverted: rgba($white, 0.85);
$gl-text-color-disabled: $gray-400;
+$link-color: $blue-500 !default;
+$link-hover-color: $blue-500 !default;
$gl-grayish-blue: #7f8fa4;
$gl-header-color: #4c4e54;
$gl-font-size-12: 12px;
@@ -440,7 +442,6 @@ $browser-scrollbar-size: 10px;
$header-height: var(--header-height, 48px);
$header-zindex: 1000;
$zindex-dropdown-menu: 300;
-$suggestion-header-height: 46px;
$ide-statusbar-height: 25px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
@@ -650,14 +651,6 @@ $calendar-border-color: rgba(#000, 0.1);
$calendar-user-contrib-text: #959494;
/*
- * Value Stream Analytics
- */
-$cycle-analytics-box-padding: 30px;
-$cycle-analytics-box-text-color: #8c8c8c;
-$cycle-analytics-big-font: 19px;
-$cycle-analytics-dismiss-icon-color: #b2b2b2;
-
-/*
* CI
*/
$ci-skipped-color: #888;
@@ -717,11 +710,11 @@ $job-arrow-margin: 55px;
*/
// See https://gitlab.com/gitlab-org/gitlab/-/issues/332150 to align with Pajamas Design System
$calendar-activity-colors: (
- #ededed,
- #acd5f2,
- #7fa8c9,
- #527ba0,
- #254e77,
+ #f5f5f5,
+ #d4dcfa,
+ #748eff,
+ #3547de,
+ #11118a,
) !default;
/*
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index cfd215b81b8..cb9c623c8fc 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -70,15 +70,6 @@
}
}
-.light-well {
- background-color: $gray-light;
- padding: 15px;
-}
-
-.dark-well {
- background-color: $gray-normal;
-}
-
.card.card-body-centered {
h1 {
font-weight: $gl-font-weight-normal;
diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss
index 5e6e10e44be..7fb2bf9a875 100644
--- a/app/assets/stylesheets/highlight/themes/dark.scss
+++ b/app/assets/stylesheets/highlight/themes/dark.scss
@@ -175,6 +175,8 @@ $dark-il: #de935f;
}
&.diff-grid-row {
+ --diff-expansion-background-color: #{$gray-600};
+
@include dark-diff-expansion-line;
}
diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss
index 19c3d6926e7..66cada9181c 100644
--- a/app/assets/stylesheets/highlight/themes/monokai.scss
+++ b/app/assets/stylesheets/highlight/themes/monokai.scss
@@ -168,6 +168,8 @@ $monokai-gh: #75715e;
}
&.diff-grid-row {
+ --diff-expansion-background-color: #{$gray-600};
+
@include dark-diff-expansion-line;
}
diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss
index 4c716d20ddf..fa1f7211b3e 100644
--- a/app/assets/stylesheets/highlight/themes/none.scss
+++ b/app/assets/stylesheets/highlight/themes/none.scss
@@ -76,6 +76,10 @@
@include match-line;
}
+ &.diff-grid-row {
+ --diff-expansion-background-color: #{$gray-100};
+ }
+
.line-coverage {
@include line-coverage-border-color($green-500, $orange-500);
}
diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
index 70086be1606..a1bba8720a2 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
@@ -171,6 +171,8 @@ $solarized-dark-il: #2aa198;
}
&.diff-grid-row {
+ --diff-expansion-background-color: #{lighten($solarized-dark-pre-bg, 10%)};
+
@include dark-diff-expansion-line;
}
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
index 8d223d1fdb1..33945f7cda9 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-light.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -156,6 +156,10 @@ $solarized-light-il: #2aa198;
@include match-line;
}
+ &.diff-grid-row {
+ --diff-expansion-background-color: #{$gray-100};
+ }
+
&.diff-grid-row.expansion .diff-td {
background-color: $solarized-light-matchline-bg;
}
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index 9761e3961dd..816aa88cfde 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -154,6 +154,8 @@ pre.code,
}
&.diff-grid-row {
+ --diff-expansion-background-color: #{$gray-100};
+
@include diff-match-line;
}
diff --git a/app/assets/stylesheets/lazy_bundles/gridstack.scss b/app/assets/stylesheets/lazy_bundles/gridstack.scss
new file mode 100644
index 00000000000..235b225d747
--- /dev/null
+++ b/app/assets/stylesheets/lazy_bundles/gridstack.scss
@@ -0,0 +1 @@
+@import 'gridstack/dist/gridstack';
diff --git a/app/assets/stylesheets/page_bundles/admin/geo_nodes.scss b/app/assets/stylesheets/page_bundles/admin/geo_nodes.scss
new file mode 100644
index 00000000000..b0aaa48569a
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/admin/geo_nodes.scss
@@ -0,0 +1,45 @@
+@import '../mixins_and_variables_and_functions';
+
+.geo-node-header-grid-columns {
+ grid-template-columns: 1fr auto;
+ grid-gap: $gl-spacing-scale-5;
+
+ @include media-breakpoint-up(md) {
+ grid-template-columns: 3fr 1fr;
+ }
+}
+
+.geo-node-details-grid-columns {
+ grid-gap: $gl-spacing-scale-5;
+
+ @include media-breakpoint-up(lg) {
+ grid-template-columns: 1fr 3fr;
+ }
+}
+
+.geo-node-core-details-grid-columns {
+ grid-template-columns: 1fr 1fr;
+ grid-gap: $gl-spacing-scale-5;
+}
+
+.geo-node-replication-details-grid-columns {
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 1rem;
+
+ @include media-breakpoint-up(md) {
+ grid-template-columns: 1fr 1fr 2fr 2fr;
+ }
+}
+
+.geo-node-filter-grid-columns {
+ grid-template-columns: 1fr;
+
+ @include media-breakpoint-up(md) {
+ grid-template-columns: 3fr 1fr;
+ }
+}
+
+.geo-node-replication-counts-grid {
+ grid-template-columns: 2fr 1fr 1fr;
+ grid-gap: 1rem;
+}
diff --git a/app/assets/stylesheets/page_bundles/admin/geo_replicable.scss b/app/assets/stylesheets/page_bundles/admin/geo_replicable.scss
new file mode 100644
index 00000000000..691d4abd195
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/admin/geo_replicable.scss
@@ -0,0 +1,18 @@
+@import '../mixins_and_variables_and_functions';
+
+.geo-replicable-item-grid {
+ grid-template-columns: 8ch 1fr auto;
+ grid-gap: 1rem;
+}
+
+.geo-replicable-filter-grid {
+ grid-template-columns: 1fr;
+
+ @include media-breakpoint-up(md) {
+ grid-template-columns: 2fr 1fr;
+ }
+
+ @include media-breakpoint-up(xl) {
+ grid-template-columns: 1fr 1fr;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/cluster_agents.scss b/app/assets/stylesheets/page_bundles/cluster_agents.scss
new file mode 100644
index 00000000000..d1fab55738f
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/cluster_agents.scss
@@ -0,0 +1,13 @@
+@import 'mixins_and_variables_and_functions';
+
+.agent-activity-list {
+ .system-note .timeline-entry-inner {
+ .timeline-icon {
+ @include gl-mt-1;
+ }
+ }
+
+ .timeline-entry::before {
+ @include gl-mt-4;
+ }
+}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/page_bundles/clusters.scss
index 27d81d8e53b..a877ae72e31 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/page_bundles/clusters.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.clusters-container {
@include media-breakpoint-down(xs) {
.nav-controls {
@@ -18,15 +20,3 @@
min-height: 372px;
}
}
-
-.agent-activity-list {
- .system-note .timeline-entry-inner {
- .timeline-icon {
- @include gl-mt-1;
- }
- }
-
- &.issuable-discussion .main-notes-list::before {
- @include gl-top-3;
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/graph_charts.scss b/app/assets/stylesheets/page_bundles/graph_charts.scss
new file mode 100644
index 00000000000..37a75f92a7e
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/graph_charts.scss
@@ -0,0 +1,27 @@
+@import 'page_bundles/mixins_and_variables_and_functions';
+
+.repo-charts {
+ .sub-header {
+ margin: 20px 0;
+ }
+
+ .sub-header-block.border-top {
+ margin-top: 20px;
+ padding: 0;
+ border-top: 1px solid var(--border-color, $border-color);
+ border-bottom: 0;
+ }
+
+ .commit-stats li {
+ font-size: 16px;
+ }
+
+ .tree-ref-header {
+ margin-bottom: 20px;
+
+ h4 {
+ margin: 0;
+ line-height: 36px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index a4a82fdcef3..3951f72112f 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -970,9 +970,6 @@ $ide-commit-header-height: 48px;
.ide-stage {
.card-header {
- display: flex;
- cursor: pointer;
-
.ci-status-icon {
display: flex;
align-items: center;
@@ -980,10 +977,6 @@ $ide-commit-header-height: 48px;
}
}
-.ide-stage-collapse-icon {
- margin: auto 0 auto auto;
-}
-
.ide-job-header {
min-height: 60px;
padding: 0 $gl-padding;
diff --git a/app/assets/stylesheets/page_bundles/incidents.scss b/app/assets/stylesheets/page_bundles/incidents.scss
new file mode 100644
index 00000000000..de246fa14b9
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/incidents.scss
@@ -0,0 +1,73 @@
+@import 'mixins_and_variables_and_functions';
+
+.issuable-discussion.incident-timeline-events {
+ .main-notes-list::before {
+ content: none;
+ }
+
+ .timeline-event-note {
+ p {
+ margin-bottom: 0;
+ font-size: 0.875rem;
+ }
+ }
+}
+
+/**
+ * We have a very specific design proposal where we cannot
+ * use `vertical-line` mixin as it is and have to use
+ * custom styles, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81284#note_904867444
+ */
+.timeline-entry-vertical-line {
+ &::before,
+ &::after {
+ content: '';
+ border-left: 2px solid $gray-50;
+ position: absolute;
+ left: 20px;
+ height: calc(100% + #{$gl-spacing-scale-5});
+ top: -#{$gl-spacing-scale-5};
+ }
+
+ &:first-child::before {
+ content: none;
+ }
+
+ &:first-child {
+ &::after {
+ top: $gl-spacing-scale-5;
+ height: calc(100% + #{$gl-spacing-scale-5});
+ }
+ }
+
+ &:last-child,
+ &.create-timeline-event {
+ &::before {
+ top: - #{$gl-spacing-scale-5} !important; // Override default positioning
+ @include gl-h-8;
+ }
+
+ &::after {
+ content: none;
+ }
+ }
+}
+
+.timeline-entry:not(:last-child) {
+ .timeline-event-border {
+ @include gl-pb-5;
+ @include gl-border-gray-50;
+ @include gl-border-1;
+ @include gl-border-b-solid;
+ }
+}
+
+.timeline-group:last-child {
+ .timeline-entry:last-child,
+ .create-timeline-event {
+ .timeline-event-bottom-border {
+ @include gl-border-b;
+ @include gl-pt-5;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/issues_show.scss b/app/assets/stylesheets/page_bundles/issues_show.scss
index 26d694f7421..bbdcf1ea0c6 100644
--- a/app/assets/stylesheets/page_bundles/issues_show.scss
+++ b/app/assets/stylesheets/page_bundles/issues_show.scss
@@ -1,5 +1,9 @@
@import 'mixins_and_variables_and_functions';
+$design-pin-diameter: 28px;
+$design-pin-diameter-sm: 24px;
+$t-gray-a-16-design-pin: rgba($black, 0.16);
+
.description {
li {
position: relative;
@@ -23,6 +27,216 @@
}
}
+.design-card-header {
+ background: transparent;
+}
+
+.design-checkbox {
+ position: absolute;
+ top: $gl-padding;
+ left: 30px;
+}
+
+.layout-page.design-detail-layout {
+ max-height: 100vh;
+}
+
+.design-detail {
+ background-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity);
+
+ .with-performance-bar & {
+ top: 35px;
+ }
+
+ .comment-indicator {
+ border-radius: 50%;
+ }
+
+ .comment-indicator,
+ .frame .design-note-pin {
+ &:active {
+ cursor: grabbing;
+ }
+ }
+}
+
+.design-list-item {
+ height: 280px;
+ text-decoration: none;
+
+ .icon-version-status {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ }
+
+ .card-body {
+ height: 230px;
+ }
+}
+
+// This is temporary class to be removed after feature flag removal: https://gitlab.com/gitlab-org/gitlab/-/issues/223197
+.design-list-item-new {
+ height: 210px;
+}
+
+.design-note-pin {
+ display: flex;
+ height: $design-pin-diameter;
+ width: $design-pin-diameter;
+ box-sizing: content-box;
+ background-color: var(--purple-500, $purple-500);
+ color: var(--white, $white);
+ font-weight: $gl-font-weight-bold;
+ border-radius: 50%;
+ z-index: 1;
+ padding: 0;
+ border: 0;
+
+ &.draft {
+ background-color: var(--orange-500, $orange-500);
+ }
+
+ &.resolved {
+ background-color: var(--gray-500, $gray-500);
+ }
+
+ &.on-image {
+ box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24;
+ border: var(--white, $white) 2px solid;
+ will-change: transform, box-shadow, opacity;
+ // NOTE: verbose transition property required for Safari
+ transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear;
+ transform-origin: 0 0;
+ transform: translate(-50%, -50%);
+
+ &:hover {
+ transform: scale(1.2) translate(-50%, -50%);
+ }
+
+ &:active {
+ box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin;
+ }
+
+ &.inactive {
+ @include gl-opacity-5;
+
+ &:hover {
+ @include gl-opacity-10;
+ }
+ }
+ }
+
+ &.small {
+ position: absolute;
+ border: 1px solid var(--white, $white);
+ height: $design-pin-diameter-sm;
+ width: $design-pin-diameter-sm;
+ }
+
+ &.user-avatar {
+ top: 25px;
+ right: 8px;
+ }
+}
+
+.design-scaler-wrapper {
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.image-notes {
+ overflow-y: scroll;
+ padding: $gl-padding;
+ padding-top: 50px;
+ background-color: var(--white, $white);
+ flex-shrink: 0;
+ min-width: 400px;
+ flex-basis: 28%;
+
+ .link-inherit-color {
+ &:hover,
+ &:active,
+ &:focus {
+ color: inherit;
+ text-decoration: none;
+ }
+ }
+
+ .toggle-comments {
+ line-height: 20px;
+ border-top: 1px solid var(--border-color, $border-color);
+
+ &.expanded {
+ border-bottom: 1px solid var(--border-color, $border-color);
+ }
+
+ .toggle-comments-button:focus {
+ text-decoration: none;
+ color: var(--blue-600, $blue-600);
+ }
+ }
+
+ .design-note-pin {
+ margin-left: $gl-padding;
+ }
+
+ .design-discussion {
+ margin: $gl-padding 0;
+
+ &::before {
+ content: '';
+ border-left: 1px solid var(--gray-100, $gray-100);
+ position: absolute;
+ left: 28px;
+ top: -17px;
+ height: 17px;
+ }
+
+ .design-note {
+ padding: $gl-padding;
+ list-style: none;
+ transition: background $gl-transition-duration-medium $general-hover-transition-curve;
+ border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box
+ border-top-right-radius: $border-radius-default;
+
+ a {
+ color: inherit;
+ }
+
+ .note-text a {
+ color: var(--blue-600, $blue-600);
+ }
+ }
+
+ .reply-wrapper {
+ padding: $gl-padding;
+ }
+ }
+
+ .reply-wrapper {
+ border-top: 1px solid var(--border-color, $border-color);
+ }
+
+ .new-discussion-disclaimer {
+ line-height: 20px;
+ }
+}
+
+@media (max-width: map-get($grid-breakpoints, lg)) {
+ .design-detail {
+ overflow-y: scroll;
+ }
+
+ .image-notes {
+ overflow-y: auto;
+ min-width: 100%;
+ flex-grow: 1;
+ flex-basis: auto;
+ }
+}
+
.is-ghost {
opacity: 0.3;
pointer-events: none;
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 463c8f74342..b2fbce7cb4b 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -9,6 +9,124 @@ $tabs-holder-z-index: 250;
min-width: 0;
}
+.diff-comment-avatar-holders {
+ position: absolute;
+ margin-left: -$gl-padding;
+ z-index: 100;
+ @include code-icon-size();
+
+ &:hover {
+ .diff-comment-avatar,
+ .diff-comments-more-count {
+ @for $i from 1 through 4 {
+ $x-pos: 14px;
+
+ &:nth-child(#{$i}) {
+ @if $i == 4 {
+ $x-pos: 14.5px;
+ }
+
+ transform: translateX((($i * $x-pos) - $x-pos));
+
+ &:hover {
+ transform: translateX((($i * $x-pos) - $x-pos));
+ }
+ }
+ }
+ }
+
+ .diff-comments-more-count {
+ padding-left: 2px;
+ padding-right: 2px;
+ width: auto;
+ }
+ }
+}
+
+.diff-comment-avatar,
+.diff-comments-more-count {
+ position: absolute;
+ left: 0;
+ margin-right: 0;
+ border-color: var(--white, $white);
+ cursor: pointer;
+ transition: all 0.1s ease-out;
+ @include code-icon-size();
+
+ @for $i from 1 through 4 {
+ &:nth-child(#{$i}) {
+ z-index: (4 - $i);
+ }
+ }
+
+ .avatar {
+ @include code-icon-size();
+ }
+}
+
+.diff-comments-more-count {
+ padding-left: 0;
+ padding-right: 0;
+ overflow: hidden;
+ @include code-icon-size();
+}
+
+.diff-file-changes {
+ max-width: 560px;
+ width: 100%;
+ z-index: 150;
+ min-height: $dropdown-min-height;
+ max-height: $dropdown-max-height;
+ overflow-y: auto;
+ margin-bottom: 0;
+
+ @include media-breakpoint-up(sm) {
+ left: $gl-padding;
+ }
+
+ .dropdown-input .dropdown-input-search {
+ pointer-events: all;
+ }
+
+ .diff-changed-file {
+ display: flex;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ min-width: 0;
+ }
+
+ .diff-file-changed-icon {
+ margin-top: 2px;
+ }
+
+ .diff-changed-file-content {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ }
+
+ .diff-changed-file-name,
+ .diff-changed-blank-file-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .diff-changed-blank-file-name {
+ color: var(--gray-400, $gray-400);
+ font-style: italic;
+ }
+
+ .diff-changed-file-path {
+ color: var(--gray-400, $gray-400);
+ }
+
+ .diff-changed-stats {
+ margin-left: auto;
+ white-space: nowrap;
+ }
+}
+
.diff-files-holder {
flex: 1;
min-width: 0;
@@ -19,6 +137,111 @@ $tabs-holder-z-index: 250;
}
}
+.diff-grid {
+ .diff-td {
+ // By default min-width is auto with 1fr which causes some overflow problems
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/296222
+ min-width: 0;
+ }
+
+ .diff-grid-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+
+ &.diff-grid-row-full {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ .diff-grid-left,
+ .diff-grid-right {
+ display: grid;
+ // Zero width column is a placeholder for the EE inline code quality diff
+ // see ee/.../diffs.scss for more details
+ grid-template-columns: 50px 8px 0 1fr;
+ }
+
+ .diff-grid-2-col {
+ grid-template-columns: 100px 1fr !important;
+
+ &.parallel {
+ grid-template-columns: 50px 1fr !important;
+ }
+ }
+
+ .diff-grid-comments {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .diff-grid-drafts {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+
+ .content + .content {
+ @include gl-border-t;
+ }
+ }
+
+ &.inline-diff-view {
+ .diff-grid-comments {
+ display: grid;
+ grid-template-columns: 1fr;
+ }
+
+ .diff-grid-drafts {
+ display: grid;
+ grid-template-columns: 1fr;
+ }
+
+ .diff-grid-row {
+ grid-template-columns: 1fr;
+ }
+
+ .diff-grid-left,
+ .diff-grid-right {
+ // Zero width column is a placeholder for the EE inline code quality diff
+ // see ee/../diffs.scss for more details
+ grid-template-columns: 50px 50px 8px 0 1fr;
+ }
+ }
+}
+
+.diff-line-expand-button {
+ &:hover,
+ &:focus {
+ background-color: var(--gray-200, $gray-200);
+ }
+}
+
+.diff-table.code .diff-tr.line_holder .diff-td.line_content.parallel {
+ width: unset;
+}
+
+.diff-tr {
+ .timeline-discussion-body {
+ clear: left;
+
+ .note-body {
+ padding: 0 0 $gl-padding-8;
+ }
+ }
+
+ .timeline-entry img.avatar {
+ margin-top: -2px;
+ margin-right: $gl-padding-8;
+ }
+
+ // tiny adjustment to vertical align with the note header text
+ .discussion-collapsible {
+ margin-left: 1rem;
+
+ .timeline-icon {
+ padding-top: 2px;
+ }
+ }
+}
+
.with-system-header {
--system-header-height: #{$system-header-height};
}
@@ -497,10 +720,6 @@ $tabs-holder-z-index: 250;
}
@include media-breakpoint-down(xs) {
- p {
- font-size: 13px;
- }
-
.btn-grouped {
float: none;
margin-right: 0;
@@ -661,10 +880,10 @@ $tabs-holder-z-index: 250;
&:not(:last-child)::before {
content: '';
- border-left: 1px solid var(--gray-100, $gray-100);
+ border-left: 2px solid var(--gray-10, $gray-10);
position: absolute;
- left: 28px;
bottom: -17px;
+ left: calc(1rem - 1px);
height: 16px;
}
}
@@ -677,7 +896,6 @@ $tabs-holder-z-index: 250;
display: flex;
align-items: center;
flex-wrap: wrap;
- padding: 16px;
z-index: 199;
white-space: nowrap;
@@ -833,6 +1051,12 @@ $tabs-holder-z-index: 250;
.detail-page-header-actions {
.gl-toggle {
@include gl-ml-auto;
+ @include gl-rounded-pill;
+ @include gl-w-9;
+
+ &.is-checked:hover {
+ background-color: $blue-500;
+ }
}
}
@@ -845,3 +1069,88 @@ $tabs-holder-z-index: 250;
@include gl-font-weight-normal;
}
}
+
+.dropdown-menu li button.gl-toggle:not(.is-checked) {
+ background: $gray-400;
+}
+
+.mr-widget-content-row:first-child {
+ border-top: 0;
+}
+
+.memory-graph-container {
+ background: var(--white, $white);
+ border: 1px solid var(--gray-100, $gray-100);
+}
+
+.review-bar-component {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ z-index: $zindex-dropdown-menu;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: $toggle-sidebar-height;
+ padding-left: $contextual-sidebar-width;
+ padding-right: $gutter_collapsed_width;
+ background: var(--white, $white);
+ border-top: 1px solid var(--border-color, $border-color);
+ transition: padding $gl-transition-duration-medium;
+
+ .page-with-icon-sidebar & {
+ padding-left: $contextual-sidebar-collapsed-width;
+ }
+
+ .right-sidebar-expanded & {
+ padding-right: $gutter_width;
+ }
+
+ @media (max-width: map-get($grid-breakpoints, sm)-1) {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ .dropdown {
+ margin-left: $grid-size;
+ }
+}
+
+.review-bar-content {
+ max-width: $limited-layout-width;
+ padding: 0 $gl-padding;
+ width: 100%;
+ margin: 0 auto;
+}
+
+.review-preview-item-header {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ margin-bottom: 4px;
+
+ > .bold {
+ display: flex;
+ min-width: 0;
+ line-height: 16px;
+ }
+}
+
+.review-preview-item-footer {
+ display: flex;
+ align-items: center;
+ margin-top: 4px;
+}
+
+.review-preview-item-content {
+ width: 100%;
+
+ p {
+ display: block;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/milestone.scss b/app/assets/stylesheets/page_bundles/milestone.scss
index c401f1a4902..63bcb83e747 100644
--- a/app/assets/stylesheets/page_bundles/milestone.scss
+++ b/app/assets/stylesheets/page_bundles/milestone.scss
@@ -1,4 +1,4 @@
-@import 'mixins_and_variables_and_functions';
+@import 'page_bundles/mixins_and_variables_and_functions';
$status-box-line-height: 26px;
@@ -40,39 +40,6 @@ $status-box-line-height: 26px;
}
}
}
-
- .card-header {
- line-height: $line-height-base;
- padding: 14px 16px;
- display: flex;
- justify-content: space-between;
-
- .title {
- flex: 1;
- flex-grow: 2;
- }
-
- .issuable-count-weight {
- white-space: nowrap;
-
- .counter,
- .weight {
- color: var(--gray-500, $gray-500);
- font-weight: $gl-font-weight-bold;
- }
- }
-
- &.text-white {
- .issuable-count-weight svg {
- fill: $white;
- }
-
- .issuable-count-weight .counter,
- .weight {
- color: var(--white, $white);
- }
- }
- }
}
.milestone-sidebar {
diff --git a/app/assets/stylesheets/components/dashboard_skeleton.scss b/app/assets/stylesheets/page_bundles/operations.scss
index 1dcaa47470b..497cb63033c 100644
--- a/app/assets/stylesheets/components/dashboard_skeleton.scss
+++ b/app/assets/stylesheets/page_bundles/operations.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.dashboard-cards {
margin-right: -$gl-padding-8;
margin-left: -$gl-padding-8;
@@ -8,7 +10,7 @@
&-header {
&-warning {
- background-color: $orange-100;
+ background-color: var(--orange-100, $orange-100);
}
}
@@ -16,16 +18,16 @@
min-height: 120px;
&-warning {
- background-color: $orange-50;
+ background-color: var(--orange-50, $orange-50);
}
&-failed {
- background-color: $red-50;
+ background-color: var(--red-50, $red-50);
}
}
&-icon {
- color: $gray-300;
+ color: var(--gray-300, $gray-300);
}
&-footer {
@@ -33,7 +35,7 @@
height: $gl-padding-32;
&-arrow {
- color: $gray-200;
+ color: var(--gray-200, $gray-200);
}
&-downstream {
@@ -41,7 +43,7 @@
}
&-extra {
- background-color: $gray-200;
+ background-color: var(--gray-200, $gray-200);
font-size: 10px;
line-height: $gl-line-height;
width: $gl-padding;
@@ -50,7 +52,7 @@
&-header {
&-failed {
- background-color: $red-100;
+ background-color: var(--red-100, $red-100);
}
}
@@ -66,10 +68,10 @@
background-repeat: no-repeat;
background-size: cover;
background-image: linear-gradient(to right,
- $gray-50 0%,
- $gray-10 20%,
- $gray-50 40%,
- $gray-50 100%);
+ var(--gray-50, $gray-50) 0%,
+ var(--gray-10, $gray-10) 20%,
+ var(--gray-50, $gray-50) 40%,
+ var(--gray-50, $gray-50) 100%);
border-radius: $gl-padding;
height: $gl-padding;
margin-top: -$gl-padding-8;
diff --git a/app/assets/stylesheets/page_bundles/pipeline_schedules.scss b/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
index 0c73bece035..af2dac7739e 100644
--- a/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
@@ -1,60 +1,82 @@
@import 'mixins_and_variables_and_functions';
-.pipeline-schedule-form {
- .gl-field-error {
- margin: 10px 0 0;
- }
+.ci-variable-list {
+ margin-left: 0;
+ margin-bottom: 0;
+ padding-left: 0;
+ list-style: none;
+ clear: both;
}
-.interval-pattern-form-group {
- label {
- margin-right: 10px;
- font-weight: $gl-font-weight-normal;
+.ci-variable-row {
+ display: flex;
+ align-items: flex-start;
- &[for='custom'] {
- margin-right: 0;
- }
+ @include media-breakpoint-down(xs) {
+ align-items: flex-end;
}
- .cron-interval-input-wrapper {
- padding-left: 0;
- }
+ &:not(:last-child) {
+ margin-bottom: $gl-btn-padding;
- .cron-interval-input {
- margin: 10px 10px 0 0;
+ @include media-breakpoint-down(xs) {
+ margin-bottom: 3 * $gl-btn-padding;
+ }
}
-}
-.pipeline-schedule-table-row {
- .branch-name-cell {
- max-width: 300px;
- }
+ &:last-child {
+ .ci-variable-body-item:last-child {
+ margin-right: $ci-variable-remove-button-width;
- a {
- color: var(--gl-text-color, $gl-text-color);
- }
+ @include media-breakpoint-down(xs) {
+ margin-right: 0;
+ }
+ }
+
+ .ci-variable-row-remove-button {
+ display: none;
+ }
- svg {
- vertical-align: middle;
+ @include media-breakpoint-down(xs) {
+ .ci-variable-row-body {
+ margin-right: $ci-variable-remove-button-width;
+ }
+ }
}
}
-.pipeline-schedules-user-callout {
- .bordered-box.content-block {
- border: 1px solid var(--border-color, $border-color);
- background-color: transparent;
+.ci-variable-row-body {
+ display: flex;
+ align-items: flex-start;
+ width: 100%;
+ padding-bottom: $gl-padding;
+
+ @include media-breakpoint-down(xs) {
+ display: block;
}
}
-.cron-preset-radio-input {
- display: inline-block;
+.ci-variable-body-item {
+ flex: 1;
- @include media-breakpoint-down(md) {
- display: block;
- margin: 0 0 5px 5px;
+ &:not(:last-child) {
+ margin-right: $gl-btn-padding;
+
+ @include media-breakpoint-down(xs) {
+ margin-right: 0;
+ margin-bottom: $gl-btn-padding;
+ }
}
+}
- input {
- margin-right: 3px;
+.pipeline-schedule-form {
+ .gl-field-error {
+ margin: 10px 0 0;
+ }
+}
+
+.pipeline-schedule-table-row {
+ a {
+ color: var(--gl-text-color, $gl-text-color);
}
}
diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index 59b8823c113..ac1e9fb024b 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -1,4 +1,33 @@
@import 'mixins_and_variables_and_functions';
+@import 'framework/buttons';
+
+.edit-user {
+ .emoji-menu-toggle-button {
+ @include emoji-menu-toggle-button;
+ }
+
+ @include media-breakpoint-down(sm) {
+ .input-md,
+ .input-lg {
+ max-width: 100%;
+ }
+ }
+}
+
+.modal-profile-crop {
+ .modal-dialog {
+ width: 380px;
+
+ @include media-breakpoint-down(xs) {
+ width: auto;
+ }
+ }
+
+ .profile-crop-image-container {
+ height: 300px;
+ margin: 0 auto;
+ }
+}
.calendar-block {
padding-left: 0;
@@ -210,3 +239,32 @@
.twitter-icon {
color: $twitter;
}
+
+.key-created-at {
+ line-height: 42px;
+}
+
+.key-list-item {
+ .key-list-item-info {
+ @include media-breakpoint-up(sm) {
+ float: left;
+ }
+ }
+}
+
+.ssh-keys-list {
+ .last-used-at,
+ .expires,
+ .key-created-at {
+ line-height: 32px;
+ }
+}
+
+.subkeys-list {
+ @include basic-list;
+
+ li {
+ padding: 3px 0;
+ border: 0;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/project.scss b/app/assets/stylesheets/page_bundles/project.scss
index eec5ebdb383..68bf2fa0f82 100644
--- a/app/assets/stylesheets/page_bundles/project.scss
+++ b/app/assets/stylesheets/page_bundles/project.scss
@@ -191,12 +191,4 @@
h5 {
color: var(--gl-text-color, $gl-text-color);
}
-
- .light-well {
- border-radius: 2px;
-
- color: var(--gray-600, $well-light-text-color);
- font-size: 13px;
- line-height: 1.6em;
- }
}
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/page_bundles/prometheus.scss
index 71cbd7d9613..702c0e4dd72 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/page_bundles/prometheus.scss
@@ -1,3 +1,11 @@
+@import 'mixins_and_variables_and_functions';
+
+.date-time-picker {
+ .date-time-picker-menu {
+ width: 400px;
+ }
+}
+
.prometheus-graphs {
.dropdown-buttons {
> div {
@@ -96,15 +104,6 @@
padding: $gl-padding-8;
}
-.alert-current-setting {
- max-width: 240px;
-
- .badge.badge-danger {
- color: $red-500;
- background-color: $red-100;
- }
-}
-
.prometheus-panel-builder {
.preview-date-time-picker {
// same as in .dropdown-menu-toggle
diff --git a/app/assets/stylesheets/components/release_block_milestone_info.scss b/app/assets/stylesheets/page_bundles/releases.scss
index b6a85ae965a..24ffbf9b90c 100644
--- a/app/assets/stylesheets/components/release_block_milestone_info.scss
+++ b/app/assets/stylesheets/page_bundles/releases.scss
@@ -1,3 +1,9 @@
+@import 'mixins_and_variables_and_functions';
+
+.release-block {
+ transition: background-color 1s linear;
+}
+
.release-block-milestone-info {
.milestone-progress-bar-container {
width: 300px;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/page_bundles/tree.scss
index a9fbff8958d..58e55e11f7e 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/page_bundles/tree.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.project-last-commit {
min-height: 4.75rem;
}
@@ -100,11 +102,11 @@
margin-bottom: 0;
tr {
- border-bottom: 1px solid $white-normal;
- border-top: 1px solid $white-normal;
+ border-bottom: 1px solid var(--gray-50, $gray-50);
+ border-top: 1px solid var(--gray-50, $gray-50);
&:last-of-type {
- border-bottom-color: $white;
+ border-bottom-color: transparent;
}
td,
@@ -117,24 +119,24 @@
}
td {
- border-color: $border-color;
+ border-color: var(--border-color, $border-color);
}
&:hover:not(.tree-truncated-warning) {
td {
- background-color: $blue-50;
+ background-color: var(--blue-50, $blue-50);
background-clip: padding-box;
- border-top: 1px solid $blue-200;
- border-bottom: 1px solid $blue-200;
+ border-top: 1px solid var(--blue-200, $blue-200);
+ border-bottom: 1px solid var(--blue-200, $blue-200);
cursor: pointer;
}
}
&.selected {
td {
- background: $white-normal;
- border-top: 1px solid $border-white-normal;
- border-bottom: 1px solid $border-white-normal;
+ background: var(--gray-50, $gray-50);
+ border-top: 1px solid var(--border-color, $border-color);
+ border-bottom: 1px solid var(--border-color, $border-color);
}
}
}
@@ -156,7 +158,7 @@
i,
a {
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
}
img {
@@ -175,22 +177,18 @@
}
.tree-truncated-warning {
- color: $orange-600;
- background-color: $orange-50;
+ color: var(--orange-600, $orange-600);
+ background-color: var(--orange-50, $orange-50);
}
.tree-time-ago {
min-width: 135px;
- color: $gl-text-color-secondary;
}
.tree-commit {
max-width: 320px;
- color: $gl-text-color-secondary;
.tree-commit-link {
- color: $gl-text-color-secondary;
-
&:hover {
text-decoration: underline;
}
@@ -207,40 +205,3 @@
.blob-content-holder {
margin-top: $gl-padding;
}
-
-.blob-upload-dropzone-previews {
- display: flex;
- justify-content: center;
- align-items: center;
- text-align: center;
- border: 2px;
- border-style: dashed;
- border-color: $border-color;
- min-height: 200px;
-}
-
-.repo-charts {
- .sub-header {
- margin: 20px 0;
- }
-
- .sub-header-block.border-top {
- margin-top: 20px;
- padding: 0;
- border-top: 1px solid $white-dark;
- border-bottom: 0;
- }
-
- .commit-stats li {
- font-size: 16px;
- }
-
- .tree-ref-header {
- margin-bottom: 20px;
-
- h4 {
- margin: 0;
- line-height: 36px;
- }
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index d0fc011dde7..820a1a0b53e 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -63,3 +63,22 @@
display: none;
}
}
+
+.work-item-dropdown {
+ .gl-dropdown-toggle {
+ background: none !important;
+
+ &:hover,
+ &:focus {
+ box-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-darkest, $gray-darkest) !important;
+ }
+
+ &.is-not-focused:not(:hover, :focus) {
+ box-shadow: none;
+
+ .gl-button-icon {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/deploy_keys.scss b/app/assets/stylesheets/pages/deploy_keys.scss
deleted file mode 100644
index 997e42a8fd5..00000000000
--- a/app/assets/stylesheets/pages/deploy_keys.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.deploy-keys-title {
- padding-bottom: 2px;
- line-height: 2;
-}
diff --git a/app/assets/stylesheets/pages/environment_logs.scss b/app/assets/stylesheets/pages/environment_logs.scss
deleted file mode 100644
index f8f40076142..00000000000
--- a/app/assets/stylesheets/pages/environment_logs.scss
+++ /dev/null
@@ -1,54 +0,0 @@
-.environment-logs-page {
- .content-wrapper {
- padding-bottom: 0;
- }
-}
-
-.environment-logs-viewer {
- height: calc(100vh - #{$environment-logs-difference-xs-up});
- min-height: 700px;
-
- @include media-breakpoint-up(md) {
- height: calc(100vh - #{$environment-logs-difference-md-up});
- }
-
- .with-performance-bar & {
- height: calc(100vh - #{$environment-logs-difference-xs-up} - #{$performance-bar-height});
-
- @include media-breakpoint-up(md) {
- height: calc(100vh - #{$environment-logs-difference-md-up} - #{$performance-bar-height});
- }
- }
-
- .top-bar {
- .date-time-picker-wrapper,
- .dropdown-toggle {
- @include media-breakpoint-up(md) {
- width: 140px;
- }
-
- @include media-breakpoint-up(lg) {
- width: 160px;
- }
- }
- }
-
- .log-lines,
- .gl-infinite-scroll-container {
- // makes scrollbar visible by creating contrast
- background: $black;
- height: 100%;
- }
-
- .build-log {
- @include build-log($black);
- }
-
- .gl-infinite-scroll-legend {
- margin: 0;
- }
-
- .build-loader-animation {
- @include build-loader-animation;
- }
-}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 33d00027404..ce8dd6684f2 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -84,6 +84,10 @@
iframe.twitter-share-button {
vertical-align: bottom;
}
+
+ .gl-label-scoped.gl-label-sm {
+ --label-inset-border: inset 0 0 0 1px currentColor;
+ }
}
code {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 85205f4d5ac..6070311dcb6 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -199,12 +199,15 @@
.sidebar-contained-width,
.issuable-sidebar-header {
width: 100%;
- border-bottom: 0;
}
.block {
@include media-breakpoint-up(lg) {
- padding: $gl-spacing-scale-5 0;
+ padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
+ }
+
+ &.participants {
+ border-bottom: 0;
}
}
}
@@ -213,7 +216,8 @@
.sidebar-contained-width,
.issuable-sidebar-header {
@include clearfix;
- padding: $gl-padding 0;
+ padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
+ border-bottom: 1px solid $border-gray-normal;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter-inner-width;
@@ -235,6 +239,13 @@
}
}
}
+
+ &.time-tracking,
+ &.participants,
+ &.subscriptions,
+ &.with-sub-blocks {
+ padding-top: $gl-spacing-scale-5;
+ }
}
.block-first {
@@ -724,13 +735,7 @@
}
.issue-check {
- padding-right: $gl-padding;
- margin-bottom: 10px;
min-width: 15px;
-
- .selected-issuable {
- vertical-align: text-top;
- }
}
.issuable-milestone,
@@ -851,24 +856,6 @@
}
}
-.issuable-todo-btn {
- .gl-spinner {
- display: none;
- }
-
- &.is-loading {
- .gl-spinner {
- display: inline-block;
- }
-
- &.sidebar-collapsed-icon {
- .issuable-todo-inner {
- display: none;
- }
- }
- }
-}
-
/*
* Following overrides are done to prevent
* legacy dropdown styles from influencing
@@ -927,88 +914,3 @@
}
}
}
-
-.icon-overlap-and-shadow {
- filter:
- drop-shadow(0 1px 0.5px #fff)
- drop-shadow(1px 0 0.5px #fff)
- drop-shadow(0 -1px 0.5px #fff)
- drop-shadow(-1px 0 0.5px #fff);
- margin-right: -7px;
- z-index: 1;
-}
-
-.issuable-discussion.incident-timeline-events {
- .main-notes-list::before {
- content: none;
- }
-
- .timeline-event-note {
- p {
- margin-bottom: 0;
- }
- }
-}
-
-/**
- * We have a very specific design proposal where we cannot
- * use `vertical-line` mixin as it is and have to use
- * custom styles, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81284#note_904867444
- */
-.timeline-entry-vertical-line {
- &::before,
- &::after {
- content: '';
- border-left: 2px solid $gray-50;
- position: absolute;
- left: 39px;
- height: calc(100% + #{$gl-spacing-scale-5});
- top: -#{$gl-spacing-scale-5};
- }
-
- &:first-child::before {
- content: none;
- }
-
- &:first-child {
- &::after {
- top: $gl-spacing-scale-5;
- height: calc(100% + #{$gl-spacing-scale-5});
- }
- }
-
- &:last-child,
- &.create-timeline-event {
- &::before {
- top: - #{$gl-spacing-scale-5} !important; // Override default positioning
- @include gl-h-8;
- }
-
- &::after {
- content: none;
- }
- }
-}
-
-.timeline-event-note-form {
- padding-left: 20px;
-}
-
-.timeline-entry:not(:last-child) {
- .timeline-event-border {
- @include gl-pb-5;
- @include gl-border-gray-50;
- @include gl-border-1;
- @include gl-border-b-solid;
- }
-}
-
-.timeline-group:last-child {
- .timeline-entry:last-child,
- .create-timeline-event {
- .timeline-event-bottom-border {
- @include gl-border-b;
- @include gl-pt-5;
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 843daec8cda..c88834c088f 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -123,6 +123,9 @@ ul.related-merge-requests > li gl-emoji {
}
.new-branch-col {
+ @include gl-pb-3;
+ @include gl-my-2;
+
.discussion-filter-container {
&:not(:last-child) {
margin-right: $gl-spacing-scale-3;
@@ -221,7 +224,7 @@ ul.related-merge-requests > li gl-emoji {
display: flex;
.new-branch-col {
- padding-top: 0;
+ @include gl-pb-0;
align-self: center;
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index fc1b78bf730..438b7b1afa6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -1,15 +1,15 @@
-$system-note-icon-size: 32px;
-$system-note-svg-size: 16px;
+$system-note-icon-size: 2rem;
+$system-note-svg-size: 1rem;
@mixin vertical-line($left) {
&::before {
content: '';
- border-left: 2px solid $gray-10;
+ border-left: 2px solid var(--gray-10, $gray-10);
position: absolute;
top: 0;
bottom: 0;
- left: $left;
- height: calc(100% - 20px);
+ left: calc(#{$left} - 1px);
+ height: calc(100% + 1.5rem);
}
}
@@ -19,17 +19,10 @@ $system-note-svg-size: 16px;
border-radius: $border-radius-default;
}
-.note-wrapper {
- padding: $gl-padding $gl-padding-8 $gl-padding $gl-padding;
-
- &.outlined {
- @include outline-comment();
- }
-}
-
-.issuable-discussion {
- .main-notes-list {
- @include vertical-line(35px);
+.issuable-discussion:not(.incident-timeline-events),
+.limited-width-notes {
+ .main-notes-list > li.timeline-entry:not(:last-of-type) {
+ @include vertical-line(1rem);
}
}
@@ -41,8 +34,6 @@ $system-note-svg-size: 16px;
position: relative;
&.timeline > .timeline-entry {
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
margin: $gl-padding 0;
&.system-note,
@@ -50,6 +41,117 @@ $system-note-svg-size: 16px;
border: 0;
}
+ .timeline-avatar {
+ height: 2rem;
+ }
+
+ &.note-comment,
+ &.note-skeleton,
+ .draft-note {
+ .timeline-avatar {
+ margin-top: 5px;
+ }
+
+ .timeline-content:not(.flash-container) {
+ margin-left: 2.5rem;
+ border: 1px solid $border-color;
+ border-radius: $gl-border-radius-base;
+ background-color: $white;
+ padding: $gl-padding-4 $gl-padding-8;
+ }
+
+ .note-header-info {
+ min-height: 2rem;
+ display: flex;
+ align-items: center;
+ gap: 0 0.25rem;
+ flex-wrap: wrap;
+ }
+ }
+
+ &.note-discussion {
+ .timeline-content .discussion-wrapper {
+ background-color: transparent;
+ }
+
+ .timeline-content {
+ ul li {
+ &:first-of-type {
+ .timeline-avatar {
+ margin-top: 5px;
+ }
+
+ .timeline-content {
+ margin-left: 2.5rem;
+ border-left: 1px solid $border-color;
+ border-right: 1px solid $border-color;
+ border-top: 1px solid $border-color;
+ border-top-left-radius: $gl-border-radius-base;
+ border-top-right-radius: $gl-border-radius-base;
+ background-color: $white;
+ padding: $gl-padding-4 $gl-padding-8;
+ }
+ }
+
+ &:not(:first-of-type) .timeline-entry-inner {
+ margin-left: 2.5rem;
+ border-left: 1px solid $border-color;
+ border-right: 1px solid $border-color;
+ background-color: $white;
+
+ .timeline-content {
+ padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
+ }
+
+ .timeline-avatar {
+ margin: $gl-padding-8 0 0 $gl-padding;
+ }
+
+ .timeline-discussion-body {
+ margin-left: 2rem;
+ }
+ }
+ }
+
+ .diff-content {
+ ul li:first-of-type {
+ .timeline-avatar {
+ margin-top: 0;
+ }
+
+ .timeline-content {
+ margin-left: 0;
+ border: 0;
+ padding: 0;
+ }
+
+ .timeline-entry-inner {
+ margin-left: 2.5rem;
+ border-left: 1px solid $border-color;
+ border-right: 1px solid $border-color;
+ background-color: $white;
+
+ .timeline-content {
+ padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
+ }
+
+ .timeline-avatar {
+ margin: $gl-padding-8 0 0 $gl-padding;
+ }
+
+ .timeline-discussion-body {
+ margin-left: 2rem;
+ }
+ }
+ }
+ }
+ }
+
+ .discussion-reply-holder {
+ border: 1px solid $border-color;
+ }
+ }
+
&.note-form {
margin-left: 0;
@@ -88,10 +190,14 @@ $system-note-svg-size: 16px;
.card {
margin-bottom: 0;
}
- }
- .timeline-discussion-body {
- margin-top: -$gl-padding-8;
+ .note-header-info {
+ min-height: 2rem;
+ display: flex;
+ align-items: center;
+ gap: 0 0.25rem;
+ flex-wrap: wrap;
+ }
}
.discussion {
@@ -116,16 +222,11 @@ $system-note-svg-size: 16px;
&.being-posted {
pointer-events: none;
opacity: 0.5;
- padding: $gl-padding;
.dummy-avatar {
background-color: $gray-100;
border: 1px solid darken($gray-100, 25%);
}
-
- .note-headline-light {
- margin-left: 3px;
- }
}
.editing-spinner {
@@ -156,6 +257,7 @@ $system-note-svg-size: 16px;
.note-edit-form {
display: block;
margin-left: 0;
+ margin-top: 0.5rem;
&.current-note-edit-form + .note-awards {
display: none;
@@ -164,13 +266,17 @@ $system-note-svg-size: 16px;
}
.note-body {
- padding: $gl-padding-4 $gl-padding-4 $gl-padding-4 $gl-padding-8;
+ padding: 0 $gl-padding-8 $gl-padding-8;
overflow-x: auto;
overflow-y: hidden;
.note-text {
word-wrap: break-word;
}
+
+ .suggestions {
+ margin-top: 4px;
+ }
}
.note-awards {
@@ -186,9 +292,10 @@ $system-note-svg-size: 16px;
}
.system-note {
- padding: $gl-padding-4 20px;
+ padding: $gl-padding-8 0;
margin: $gl-padding 0;
background-color: transparent;
+ font-size: $gl-font-size;
.note-header-info {
padding-bottom: 0;
@@ -229,6 +336,15 @@ $system-note-svg-size: 16px;
.note-body {
overflow: hidden;
+ padding: 0;
+
+ ul {
+ margin: 0.5rem 0;
+ }
+
+ p {
+ margin-left: 1rem;
+ }
.description-version {
position: relative;
@@ -305,7 +421,7 @@ $system-note-svg-size: 16px;
height: $system-note-icon-size;
border: 1px solid $gray-10;
border-radius: $system-note-icon-size;
- margin: -6px 0 0;
+ margin: -8px 0 0;
svg {
width: $system-note-svg-size;
@@ -319,25 +435,38 @@ $system-note-svg-size: 16px;
.discussion-filter-note {
.timeline-icon {
- width: $system-note-icon-size + 6;
- height: $system-note-icon-size + 6;
+ width: $system-note-icon-size;
+ height: $system-note-icon-size;
margin-top: -8px;
}
}
}
+.card .notes {
+ .system-note {
+ margin: 0;
+ padding: 0;
+ }
+
+ .timeline-icon {
+ margin: 8px 0 0 14px;
+ }
+}
+
+
// Diff code in discussion view
.discussion-body .diff-file {
.file-title {
cursor: default;
- border-top: 1px solid $border-color;
+ border-top: 0;
border-radius: 0;
+ margin-left: 2.5rem;
@media (min-width: map-get($grid-breakpoints, md)) {
--initial-top: calc(#{$header-height} + #{$mr-tabs-height});
&.is-sidebar-moved {
- --initial-top: calc(#{$header-height} + #{$mr-tabs-height + 28px});
+ --initial-top: calc(#{$header-height} + #{$mr-tabs-height + 24px});
}
.with-performance-bar & {
@@ -357,6 +486,40 @@ $system-note-svg-size: 16px;
.line_content {
white-space: pre-wrap;
}
+
+ .diff-content {
+ margin-left: 2.5rem;
+
+ &.outdated-lines-wrapper {
+ margin-left: 0;
+ }
+
+ .line_holder td:first-of-type {
+ @include gl-border-l;
+ }
+
+ .line_holder td:last-of-type {
+ @include gl-border-r;
+ }
+
+ .discussion-notes {
+ margin-left: -2.5rem;
+
+ .notes {
+ background-color: transparent;
+ }
+
+ .notes-content {
+ border: 0;
+ }
+
+ .timeline-content {
+ border-top: 0 !important;
+ border-top-left-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+ }
+ }
+ }
}
.tab-pane.notes {
@@ -394,8 +557,17 @@ $system-note-svg-size: 16px;
}
.system-note {
- background-color: $white;
- padding: $gl-padding;
+ background-color: transparent;
+ padding: 0;
+
+ .timeline-icon {
+ margin-top: -2px;
+ }
+
+ .timeline-entry-inner .timeline-icon {
+ margin-top: $grid-size;
+ margin-left: 14px;
+ }
}
}
@@ -487,6 +659,19 @@ $system-note-svg-size: 16px;
.code-commit .notes-content,
.diff-viewer > .image ~ .note-container {
background-color: $white;
+
+ li.note-comment {
+ padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
+
+ .avatar {
+ margin-right: 0;
+ }
+
+ .note-body {
+ padding: $gl-padding-4 0 $gl-padding-8;
+ margin-left: 2.5rem;
+ }
+ }
}
.diff-viewer > .image ~ .note-container form.new-note {
@@ -540,9 +725,21 @@ $system-note-svg-size: 16px;
padding-bottom: 0;
}
+ .timeline-avatar {
+ margin-top: 5px;
+ }
+
.timeline-content {
overflow-x: auto;
overflow-y: hidden;
+ border-radius: $gl-border-radius-base;
+ padding: $gl-padding-8 !important;
+ @include gl-border;
+
+ &.expanded {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
}
&.note-wrapper {
@@ -568,19 +765,10 @@ $system-note-svg-size: 16px;
.note {
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
.note-header {
- .note-actions {
- flex-wrap: wrap;
- margin-bottom: $gl-padding-12;
-
- > :first-child {
- margin-left: 0;
- }
+ .note-actions > :first-child {
+ margin-left: 0;
}
}
-
- .note-header-author-name {
- display: block;
- }
}
}
@@ -593,11 +781,6 @@ $system-note-svg-size: 16px;
}
}
-.note-header-info,
-.note-actions {
- padding-bottom: $gl-padding-4;
-}
-
.system-note .note-header-info {
padding-bottom: 0;
}
@@ -618,10 +801,6 @@ $system-note-svg-size: 16px;
}
.note-headline-meta {
- .system-note-separator {
- color: $gray-500;
- }
-
.note-timestamp {
white-space: nowrap;
}
@@ -667,18 +846,20 @@ $system-note-svg-size: 16px;
}
.note-actions {
- align-self: flex-start;
justify-content: flex-end;
flex-shrink: 1;
display: inline-flex;
align-items: center;
- margin-left: 10px;
+ margin-left: $gl-padding-8;
color: $gray-400;
- margin-top: -4px;
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
+ justify-content: flex-start;
float: none;
- margin-left: 0;
+
+ .note-actions__mobile-spacer {
+ flex-grow: 1;
+ }
}
}
@@ -719,7 +900,7 @@ $system-note-svg-size: 16px;
}
.discussion-toggle-button {
- padding: 0;
+ padding: 0 $gl-padding-8 0 0;
background-color: transparent;
border: 0;
line-height: 20px;
@@ -868,6 +1049,28 @@ $system-note-svg-size: 16px;
.note-discussion.timeline-entry {
padding-left: 0;
+ ul.notes li.note-wrapper {
+ .timeline-content {
+ padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
+ }
+
+ .timeline-avatar {
+ margin: $gl-padding-8 0 0 $gl-padding;
+ }
+ }
+
+ ul.notes {
+ li.toggle-replies-widget {
+ margin-left: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+ div.discussion-reply-holder {
+ margin-left: 0;
+ }
+ }
+
&:last-child {
border-bottom: 0;
}
@@ -894,6 +1097,16 @@ $system-note-svg-size: 16px;
}
}
+ .draft-note-component .draft-note.timeline-entry {
+ .timeline-content:not(.flash-container) {
+ padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
+ }
+
+ .timeline-avatar {
+ margin: $gl-padding-8 0 0 $gl-padding;
+ }
+ }
+
.diff-comment-form {
display: block;
}
@@ -909,8 +1122,7 @@ $system-note-svg-size: 16px;
// See https://gitlab.com/gitlab-org/gitlab-foss/issues/53918#note_117038785
.unstyled-comments {
.discussion-header {
- padding: $gl-padding;
- border-bottom: 1px solid $border-color;
+ padding: $gl-padding 0;
}
.discussion-form-container {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 951e31ef768..8e4dd39e498 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -1,22 +1,3 @@
-.avatar-image {
- margin-bottom: $grid-size;
-
- .avatar {
- float: none;
- }
-
- @include media-breakpoint-up(sm) {
- float: left;
- margin-bottom: 0;
- }
-}
-
-.avatar-file-name {
- position: relative;
- top: 2px;
- display: inline-block;
-}
-
.account-well {
padding: 10px;
background-color: $gray-light;
@@ -29,42 +10,6 @@
}
}
-.user-avatar-button {
- .file-name {
- display: inline-block;
- padding-left: 10px;
- }
-}
-
-.subkeys-list {
- @include basic-list;
-
- li {
- padding: 3px 0;
- border: 0;
- }
-}
-
-.key-list-item {
- .key-list-item-info {
- @include media-breakpoint-up(sm) {
- float: left;
- }
- }
-}
-
-.ssh-keys-list {
- .last-used-at,
- .expires,
- .key-created-at {
- line-height: 32px;
- }
-}
-
-.key-created-at {
- line-height: 42px;
-}
-
.provider-btn-group {
display: inline-block;
margin-right: 10px;
@@ -113,26 +58,6 @@
}
}
-.modal-profile-crop {
- .modal-dialog {
- width: 380px;
-
- @include media-breakpoint-down(xs) {
- width: auto;
- }
- }
-
- .profile-crop-image-container {
- height: 300px;
- margin: 0 auto;
- }
-
- .crop-controls {
- padding: 10px 0 0;
- text-align: center;
- }
-}
-
.created-personal-access-token-container {
.btn-clipboard {
border: 1px solid $border-color;
@@ -247,36 +172,6 @@ table.u2f-registrations {
}
}
-.edit-user {
- svg {
- fill: $gl-text-color-secondary;
- }
-
- .form-group > label {
- font-weight: $gl-font-weight-bold;
- }
-
- .form-group > .form-text {
- font-size: $gl-font-size;
- }
-
- .emoji-menu-toggle-button {
- @include emoji-menu-toggle-button;
- padding: 6px 10px;
-
- .no-emoji-placeholder {
- position: relative;
- }
- }
-
- @include media-breakpoint-down(sm) {
- .input-md,
- .input-lg {
- max-width: 100%;
- }
- }
-}
-
.help-block {
color: $gl-text-color-secondary;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 0d45beab983..be8707dcd50 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -269,49 +269,27 @@
color: $gray-200;
}
-pre.light-well {
- border-color: $well-light-border;
-}
-
/*
* Projects list rendered on dashboard and user page
*/
+
+.project-row {
+ .description p {
+ margin-bottom: 0;
+ color: $gl-text-color-secondary;
+ }
+}
+
.projects-list {
@include basic-list;
display: flex;
flex-direction: column;
- // Disable Flexbox for admin page
- &.admin-projects,
- &.group-settings-projects {
- display: block;
-
- .project-row {
- display: block;
-
- .description > p {
- margin-bottom: 0;
- }
- }
- }
-
.project-row {
@include basic-list-stats;
display: flex;
align-items: center;
padding: $gl-padding-12 0;
-
- &.no-description {
- @include media-breakpoint-up(sm) {
- .avatar-container {
- align-self: center;
- }
-
- .metadata-info {
- margin-bottom: 0;
- }
- }
- }
}
h2 {
@@ -634,24 +612,6 @@ pre.light-well {
}
}
-.clearable-input {
- position: relative;
-
- .clear-icon {
- display: none;
- position: absolute;
- right: 9px;
- top: 9px;
- }
-
- &.has-value {
- .clear-icon {
- cursor: pointer;
- display: block;
- }
- }
-}
-
.project-path {
.form-control {
min-width: 100px;
@@ -810,10 +770,3 @@ pre.light-well {
}
}
}
-
-@include media-breakpoint-down(xs) {
- .fork-filtered-search {
- width: 100%;
- margin: $gl-spacing-scale-2 0;
- }
-}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index e8f71c8a21c..a8027d2a5f5 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -268,16 +268,6 @@ input[type='checkbox']:hover {
}
}
- .search-clear {
- position: absolute;
- right: 10px;
- top: 9px;
- padding: 0;
- line-height: 0;
- background: none;
- border: 0;
- }
-
.search-icon {
position: absolute;
left: 10px;
@@ -327,15 +317,6 @@ input[type='checkbox']:hover {
}
}
-.search-clear {
- color: $gray-darkest;
-
- &:hover,
- &:focus {
- color: $blue-600;
- }
-}
-
.search-page-form {
.dropdown-menu-toggle,
.btn-search {
diff --git a/app/assets/stylesheets/pages/service_desk.scss b/app/assets/stylesheets/pages/service_desk.scss
deleted file mode 100644
index 34ab5eb1b74..00000000000
--- a/app/assets/stylesheets/pages/service_desk.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.service-desk-issues {
- .non-empty-state {
- text-align: left;
- padding-bottom: $gl-padding-top;
- border-bottom: 1px solid $border-color;
- }
-}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 56acf6de828..c364b233803 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -188,31 +188,6 @@
}
}
-.nested-settings {
- padding-left: 20px;
-}
-
-.input-btn-group {
- display: flex;
-
- .input-large {
- flex: 1;
- }
-
- .btn {
- margin-left: 10px;
- }
-}
-
-.content-list > .settings-flex-row {
- display: flex;
- align-items: center;
-
- .float-right {
- margin-left: auto;
- }
-}
-
.prometheus-metrics-monitoring {
.card {
.card-toggle {
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index 0450b3d9a44..32c3ce1ba8c 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -57,7 +57,7 @@ strong {
font-weight: bolder;
}
a {
- color: #007bff;
+ color: #428fdc;
text-decoration: none;
background-color: transparent;
}
@@ -368,6 +368,23 @@ kbd kbd {
white-space: nowrap;
border: 0;
}
+.gl-avatar {
+ border-width: 1px;
+ border-style: solid;
+ border-color: rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+ flex-shrink: 0;
+}
+.gl-avatar-s24 {
+ width: 1.5rem;
+ height: 1.5rem;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ border-radius: 0.25rem;
+}
+.gl-avatar-circle {
+ border-radius: 50%;
+}
.gl-badge {
display: inline-flex;
align-items: center;
@@ -552,9 +569,6 @@ html [type="button"],
strong {
font-weight: bold;
}
-a {
- color: #63a6e9;
-}
svg {
vertical-align: baseline;
}
@@ -1783,10 +1797,15 @@ body.gl-dark {
background-color: #262626;
border-right: 1px solid #303030;
}
+.gl-avatar:not(.gl-avatar-identicon),
.avatar-container,
.avatar {
background: rgba(255, 255, 255, 0.04);
}
+.gl-avatar {
+ border-style: none;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
+}
body.gl-dark {
--gl-theme-accent: #868686;
}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 356fb58b4c8..61a2ce8dd62 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -38,7 +38,7 @@ strong {
font-weight: bolder;
}
a {
- color: #007bff;
+ color: #1f75cb;
text-decoration: none;
background-color: transparent;
}
@@ -349,6 +349,23 @@ kbd kbd {
white-space: nowrap;
border: 0;
}
+.gl-avatar {
+ border-width: 1px;
+ border-style: solid;
+ border-color: rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+ flex-shrink: 0;
+}
+.gl-avatar-s24 {
+ width: 1.5rem;
+ height: 1.5rem;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ border-radius: 0.25rem;
+}
+.gl-avatar-circle {
+ border-radius: 50%;
+}
.gl-badge {
display: inline-flex;
align-items: center;
@@ -533,9 +550,6 @@ html [type="button"],
strong {
font-weight: bold;
}
-a {
- color: #1068bf;
-}
svg {
vertical-align: baseline;
}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index edc579f48f6..33e10b9bd62 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -41,7 +41,7 @@ p {
margin-bottom: 1rem;
}
a {
- color: #007bff;
+ color: #1f75cb;
text-decoration: none;
background-color: transparent;
}
@@ -498,7 +498,7 @@ input.btn-block[type="button"] {
.custom-control-input:checked:disabled
~ .custom-control-label::before,
.gl-form-checkbox.custom-control
- .custom-control-input:indeterminate:disabled
+ .custom-control-input[type="checkbox"]:indeterminate:disabled
~ .custom-control-label::before {
background-color: #dbdbdb;
border-color: #dbdbdb;
@@ -507,7 +507,7 @@ input.btn-block[type="button"] {
.custom-control-input:checked:disabled
~ .custom-control-label::after,
.gl-form-checkbox.custom-control
- .custom-control-input:indeterminate:disabled
+ .custom-control-input[type="checkbox"]:indeterminate:disabled
~ .custom-control-label::after {
background-color: #5e5e5e;
}
@@ -595,9 +595,6 @@ h3 {
margin-top: 20px;
margin-bottom: 10px;
}
-a {
- color: #1068bf;
-}
hr {
overflow: hidden;
}
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 4b74e449e06..8e8cabbe511 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -269,9 +269,9 @@ $well-expand-item: $gray-200;
$well-inner-border: $gray-200;
$calendar-activity-colors: (
- #303030,
- #333861,
- #4a5593,
- #6172c5,
- #788ff7
+ #404040,
+ #1e23a8,
+ #445cf2,
+ #97acff,
+ #e9ebff
);
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index e1ba2a69420..a0d19c3de2a 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -141,7 +141,8 @@ body.gl-dark {
}
}
-.timeline-entry.internal-note:not(.note-form) {
+.timeline-entry.internal-note:not(.note-form) .timeline-content,
+.timeline-entry.draft-note:not(.note-form) .timeline-content {
// soften on darkmode
- background-color: mix($gray-50, $orange-50, 75%);
+ background-color: mix($gray-50, $orange-50, 75%) !important;
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index bdb8f758137..4be4fc82d04 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -75,10 +75,8 @@
// .gl-font-size-inherit will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1466
.gl-font-size-inherit,
.font-size-inherit { font-size: inherit; }
-.gl-w-8 { width: px-to-rem($grid-size); }
.gl-w-16 { width: px-to-rem($grid-size * 2); }
.gl-w-64 { width: px-to-rem($grid-size * 8); }
-.gl-h-8 { height: px-to-rem($grid-size); }
.gl-h-32 { height: px-to-rem($grid-size * 4); }
.gl-h-64 { height: px-to-rem($grid-size * 8); }
@@ -119,13 +117,6 @@
flex-basis: 25%;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1168
-.gl-md-ml-3 {
- @media (min-width: $breakpoint-md) {
- margin-left: $gl-spacing-scale-3;
- }
-}
-
// Will be moved to @gitlab/ui (without the !important) in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1462
// We only need the bang (!) version until the non-bang version is added to
// @gitlab/ui utitlities.scss. Once there, it will get loaded in the correct
@@ -152,48 +143,6 @@
display: flex;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1085
-.gl-md-flex-direction-column {
- @media (min-width: $breakpoint-md) {
- flex-direction: column;
- }
-}
-
-// Same as above
-.gl-md-flex-direction-column\! {
- @media (min-width: $breakpoint-md) {
- flex-direction: column !important;
- }
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1165
-.gl-xs-mb-4 {
- @media (max-width: $breakpoint-sm) {
- margin-bottom: $gl-spacing-scale-4;
- }
-}
-
-// Same as above
-.gl-xs-mb-4\! {
- @media (max-width: $breakpoint-sm) {
- margin-bottom: $gl-spacing-scale-4 !important;
- }
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1168
-.gl-sm-pr-3 {
- @media (min-width: $breakpoint-sm) {
- padding-right: $gl-spacing-scale-3;
- }
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1168
-.gl-sm-w-half {
- @media (min-width: $breakpoint-sm) {
- width: 50%;
- }
-}
-
.gl-sm-mr-3 {
@include media-breakpoint-up(sm) {
margin-right: $gl-spacing-scale-3;
@@ -206,21 +155,10 @@
}
}
-.gl-mb-n3 {
- margin-bottom: -$gl-spacing-scale-3;
-}
-
.gl-mr-n2 {
margin-right: -$gl-spacing-scale-2;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1408
-$gl-line-height-42: px-to-rem(42px);
-
-.gl-line-height-42 {
- line-height: $gl-line-height-42;
-}
-
.gl-w-grid-size-30 {
width: $grid-size * 30;
}
@@ -229,26 +167,6 @@ $gl-line-height-42: px-to-rem(42px);
width: $grid-size * 40;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2209
-.gl-max-w-none\! {
- max-width: none !important;
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2209
-.gl-max-h-none\! {
- max-height: none !important;
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1655
-.gl-max-w-62 {
- max-width: $grid-size * 62;
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1655
-.gl-max-w-26 {
- max-width: $grid-size * 26;
-}
-
.gl-max-w-50p {
max-width: 50%;
}
@@ -271,36 +189,15 @@ $gl-line-height-42: px-to-rem(42px);
}
}
-// Will both be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
-.gl-text-transparent {
- color: transparent;
-}
-
+// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
.gl-focus-ring-border-1-gray-900\! {
@include gl-focus($gl-border-size-1, $gray-900, true);
}
-// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2600
-.gl-pr-10 {
- padding-right: $gl-spacing-scale-10;
-}
-
/*
All of the following (up until the "End gitlab-ui#1709" comment) will be moved
to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
*/
-.gl-sm-grid-template-columns-2 {
- @include media-breakpoint-up(sm) {
- grid-template-columns: 1fr 1fr;
- }
-}
-
-.gl-md-grid-template-columns-2 {
- @include media-breakpoint-up(md) {
- grid-template-columns: 1fr 1fr;
- }
-}
-
.gl-md-grid-template-columns-3 {
@include media-breakpoint-up(md) {
grid-template-columns: repeat(3, 1fr);
@@ -313,10 +210,6 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
}
}
-.gl-gap-6 {
- gap: $gl-spacing-scale-6;
-}
-
.gl-max-w-48 {
max-width: $gl-spacing-scale-48;
}
@@ -346,18 +239,6 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
/* End gitlab-ui#1709 */
/*
- * The below two styles will be moved to @gitlab/ui by
- * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1750
- */
-.gl-max-w-34 {
- max-width: 34 * $grid-size;
-}
-
-.gl-max-w-80 {
- max-width: 80 * $grid-size;
-}
-
-/*
* The below style will be moved to @gitlab/ui by
* https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1751
*/
@@ -370,13 +251,3 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
.gl-flex-flow-row-wrap {
flex-flow: row wrap;
}
-
-/*
- * The below style will be moved to @gitlab/ui by
- * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1963
- */
-.gl-gap-y-3 {
- > * + * {
- margin-top: $gl-spacing-scale-3;
- }
-}
diff --git a/app/components/pajamas/alert_component.rb b/app/components/pajamas/alert_component.rb
index cfab34f537e..4475f4cde6e 100644
--- a/app/components/pajamas/alert_component.rb
+++ b/app/components/pajamas/alert_component.rb
@@ -12,8 +12,8 @@ module Pajamas
def initialize(
title: nil, variant: :info, dismissible: true, show_icon: true,
alert_options: {}, close_button_options: {})
- @title = title
- @variant = variant
+ @title = title.presence
+ @variant = filter_attribute(variant&.to_sym, VARIANT_ICONS.keys, default: :info)
@dismissible = dismissible
@show_icon = show_icon
@alert_options = alert_options
@@ -35,7 +35,7 @@ module Pajamas
renders_one :body
renders_one :actions
- ICONS = {
+ VARIANT_ICONS = {
info: 'information-o',
warning: 'warning',
success: 'check-circle',
@@ -44,7 +44,7 @@ module Pajamas
}.freeze
def icon
- ICONS[@variant]
+ VARIANT_ICONS[@variant]
end
def icon_classes
diff --git a/app/components/pajamas/progress_component.html.haml b/app/components/pajamas/progress_component.html.haml
new file mode 100644
index 00000000000..9368fe8b161
--- /dev/null
+++ b/app/components/pajamas/progress_component.html.haml
@@ -0,0 +1,2 @@
+.progress
+ .progress-bar{ class: "bg-#{@variant}", style: "width: #{@value}%;" }
diff --git a/app/components/pajamas/progress_component.rb b/app/components/pajamas/progress_component.rb
new file mode 100644
index 00000000000..1365da13863
--- /dev/null
+++ b/app/components/pajamas/progress_component.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Pajamas
+ class ProgressComponent < Pajamas::Component
+ def initialize(value: 0, variant: :primary)
+ @value = value
+ @variant = filter_attribute(variant, VARIANT_OPTIONS, default: :primary)
+ end
+
+ VARIANT_OPTIONS = [:primary, :success].freeze
+ end
+end
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 251ba9e29f2..edd85414696 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -10,6 +10,9 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def index
+ push_frontend_feature_flag(:vue_broadcast_messages, current_user)
+ push_frontend_feature_flag(:role_targeted_broadcast_messages, current_user)
+
@broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page])
@broadcast_message = BroadcastMessage.new
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 2ae0442c005..f3c4244269d 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -60,17 +60,6 @@ class Admin::GroupsController < Admin::ApplicationController
end
end
- def members_update
- member_params = params.permit(:user_id, :access_level, :expires_at)
- result = Members::CreateService.new(current_user, member_params.merge(limit: -1, source: @group, invite_source: 'admin-group-page')).execute
-
- if result[:status] == :success
- redirect_to [:admin, @group], notice: _('Users were successfully added.')
- else
- redirect_to [:admin, @group], alert: result[:message]
- end
- end
-
def destroy
Groups::DestroyService.new(@group, current_user).async_execute
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index eb279298baf..9d884478e98 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -14,11 +14,10 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
@impersonation_token = finder.build(impersonation_token_params)
if @impersonation_token.save
- PersonalAccessToken.redis_store!(current_user.id, @impersonation_token.token)
- redirect_to admin_user_impersonation_tokens_path, notice: _("A new impersonation token has been created.")
+ render json: { new_token: @impersonation_token.token,
+ active_access_tokens: active_impersonation_tokens }, status: :ok
else
- set_index_vars
- render :index
+ render json: { errors: @impersonation_token.errors.full_messages }, status: :unprocessable_entity
end
end
@@ -50,19 +49,19 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
end
+ def active_impersonation_tokens
+ tokens = finder(state: 'active', sort: 'expires_at_asc_id_desc').execute
+ ::ImpersonationAccessTokenSerializer.new.represent(tokens)
+ end
+
def impersonation_token_params
params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: [])
end
- # rubocop: disable CodeReuse/ActiveRecord
def set_index_vars
@scopes = Gitlab::Auth.available_scopes_for(current_user)
@impersonation_token ||= finder.build
- @inactive_impersonation_tokens = finder(state: 'inactive').execute
- @active_impersonation_tokens = finder(state: 'active').execute.order(:expires_at)
-
- @new_impersonation_token = PersonalAccessToken.redis_getdel(current_user.id)
+ @active_impersonation_tokens = active_impersonation_tokens
end
- # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index a0f72f5e58c..96fe0c9331d 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -4,13 +4,6 @@ class Admin::RunnersController < Admin::ApplicationController
include RunnerSetupScripts
before_action :runner, except: [:index, :tag_list, :runner_setup_scripts]
- before_action only: [:index] do
- push_frontend_feature_flag(:admin_runners_bulk_delete)
- end
-
- before_action only: [:show] do
- push_frontend_feature_flag(:enforce_runner_token_expires_at)
- end
feature_category :runner
urgency :low
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 71d9910b4b8..84efb8b0da8 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -16,7 +16,6 @@ class ApplicationController < ActionController::Base
include SessionlessAuthentication
include SessionsHelper
include ConfirmEmailWarning
- include Gitlab::Experimentation::ControllerConcern
include InitializesCurrentUserMode
include Impersonation
include Gitlab::Logging::CloudflareHelper
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 88592efcec7..45585ab84b4 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -25,7 +25,20 @@ class AutocompleteController < ApplicationController
.new(params: params, current_user: current_user, project: project, group: group)
.execute
- render json: UserSerializer.new(params.merge({ current_user: current_user })).represent(users, project: project)
+ presented_users = UserSerializer
+ .new(params.merge({ current_user: current_user }))
+ .represent(users, project: project)
+
+ extra_users = presented_suggested_users
+
+ if extra_users.present?
+ presented_users.reject! do |user|
+ extra_users.any? { |suggested_user| suggested_user[:id] == user[:id] }
+ end
+ presented_users += extra_users
+ end
+
+ render json: presented_users
end
def user
@@ -80,6 +93,11 @@ class AutocompleteController < ApplicationController
def target_branch_params
params.permit(:group_id, :project_id).select { |_, v| v.present? }
end
+
+ # overridden in EE
+ def presented_suggested_users
+ []
+ end
end
AutocompleteController.prepend_mod_with('AutocompleteController')
diff --git a/app/controllers/boards/application_controller.rb b/app/controllers/boards/application_controller.rb
deleted file mode 100644
index 15ef6698472..00000000000
--- a/app/controllers/boards/application_controller.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Boards
- class ApplicationController < ::ApplicationController
- respond_to :json
-
- rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
-
- private
-
- def board
- @board ||= Board.find(params[:board_id])
- end
-
- def board_parent
- @board_parent ||= board.resource_parent
- end
-
- def record_not_found(exception)
- render json: { error: exception.message }, status: :not_found
- end
- end
-end
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
deleted file mode 100644
index 5028544795c..00000000000
--- a/app/controllers/boards/issues_controller.rb
+++ /dev/null
@@ -1,162 +0,0 @@
-# frozen_string_literal: true
-
-module Boards
- class IssuesController < Boards::ApplicationController
- # This is the maximum amount of issues which can be moved by one request to
- # bulk_move for now. This is temporary and might be removed in future by
- # introducing an alternative (async?) approach.
- # (related: https://gitlab.com/groups/gitlab-org/-/epics/382)
- MAX_MOVE_ISSUES_COUNT = 50
-
- include BoardsResponses
- include ControllerWithCrossProjectAccessCheck
-
- requires_cross_project_access if: -> { board&.group_board? }
-
- before_action :disable_query_limiting, only: [:bulk_move]
- before_action :authorize_read_issue, only: [:index]
- before_action :authorize_create_issue, only: [:create]
- before_action :authorize_update_issue, only: [:update]
- skip_before_action :authenticate_user!, only: [:index]
- before_action :validate_id_list, only: [:bulk_move]
- before_action :can_move_issues?, only: [:bulk_move]
-
- feature_category :team_planning
- urgency :low
-
- def index
- list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
- issues = issues_from(list_service)
-
- ::Boards::Issues::ListService.initialize_relative_positions(board, current_user, issues)
-
- render_issues(issues, list_service.metadata)
- end
-
- def create
- service = Boards::Issues::CreateService.new(board_parent, project, current_user, issue_params)
- issue = service.execute
-
- if issue.valid?
- render json: serialize_as_json(issue)
- else
- render json: issue.errors, status: :unprocessable_entity
- end
- end
-
- def bulk_move
- service = Boards::Issues::MoveService.new(board_parent, current_user, move_params(true))
-
- issues = Issue.find(params[:ids])
-
- render json: service.execute_multiple(issues)
- end
-
- def update
- service = Boards::Issues::MoveService.new(board_parent, current_user, move_params)
-
- if service.execute(issue)
- head :ok
- else
- head :unprocessable_entity
- end
- end
-
- private
-
- def issues_from(list_service)
- issues = list_service.execute
- issues.page(params[:page]).per(params[:per] || 20)
- .without_count
- .preload(associations_to_preload) # rubocop: disable CodeReuse/ActiveRecord
- .load
- end
-
- def associations_to_preload
- [
- :milestone,
- :assignees,
- project: [
- :route,
- {
- namespace: [:route]
- }
- ],
- labels: [:priorities],
- notes: [:award_emoji, :author]
- ]
- end
-
- def can_move_issues?
- head(:forbidden) unless can?(current_user, :admin_issue, board)
- end
-
- def serializer_options(issues)
- {}
- end
-
- def render_issues(issues, metadata)
- data = { issues: serialize_as_json(issues, opts: serializer_options(issues)) }
- data.merge!(metadata)
-
- render json: data
- end
-
- def issue
- @issue ||= issues_finder.find(params[:id])
- end
-
- def filter_params
- params.permit(*Boards::Issues::ListService.valid_params).merge(board_id: params[:board_id], id: params[:list_id])
- .reject { |_, value| value.nil? }
- end
-
- def issues_finder
- if board.group_board?
- IssuesFinder.new(current_user, group_id: board_parent.id)
- else
- IssuesFinder.new(current_user, project_id: board_parent.id)
- end
- end
-
- def project
- @project ||= if board.group_board?
- Project.find(issue_params[:project_id])
- else
- board_parent
- end
- end
-
- def move_params(multiple = false)
- id_param = multiple ? :ids : :id
- params.permit(id_param, :board_id, :from_list_id, :to_list_id, :move_before_id, :move_after_id)
- end
-
- def issue_params
- params.require(:issue)
- .permit(:title, :milestone_id, :project_id)
- .merge(board_id: params[:board_id], list_id: params[:list_id])
- end
-
- def serializer
- IssueSerializer.new(current_user: current_user)
- end
-
- def serialize_as_json(resource, opts: {})
- opts.merge!(include_full_project_path: board.group_board?, serializer: 'board')
-
- serializer.represent(resource, opts)
- end
-
- def disable_query_limiting
- Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/issues/35174')
- end
-
- def validate_id_list
- head(:bad_request) unless params[:ids].is_a?(Array)
- head(:unprocessable_entity) if params[:ids].size > MAX_MOVE_ISSUES_COUNT
- end
- end
-end
-
-Boards::IssuesController.prepend_mod_with('Boards::IssuesController')
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
deleted file mode 100644
index c3b5a887920..00000000000
--- a/app/controllers/boards/lists_controller.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-# frozen_string_literal: true
-
-module Boards
- class ListsController < Boards::ApplicationController
- include BoardsResponses
-
- before_action :authorize_admin_list, only: [:create, :destroy, :generate]
- before_action :authorize_read_list, only: [:index]
- skip_before_action :authenticate_user!, only: [:index]
-
- feature_category :team_planning
- urgency :low
-
- def index
- lists = Boards::Lists::ListService.new(board.resource_parent, current_user).execute(board)
-
- List.preload_preferences_for_user(lists, current_user)
-
- render json: serialize_as_json(lists)
- end
-
- def create
- response = Boards::Lists::CreateService.new(board.resource_parent, current_user, create_list_params).execute(board)
-
- if response.success?
- render json: serialize_as_json(response.payload[:list])
- else
- render json: { errors: response.errors }, status: :unprocessable_entity
- end
- end
-
- def update
- list = board.lists.find(params[:id])
- service = Boards::Lists::UpdateService.new(board_parent, current_user, update_list_params)
- result = service.execute(list)
-
- if result.success?
- head :ok
- else
- head result.http_status
- end
- end
-
- def destroy
- list = board.lists.destroyable.find(params[:id])
- service = Boards::Lists::DestroyService.new(board_parent, current_user)
-
- if service.execute(list).success?
- head :ok
- else
- head :unprocessable_entity
- end
- end
-
- def generate
- service = Boards::Lists::GenerateService.new(board_parent, current_user)
-
- if service.execute(board)
- lists = board.lists.movable.preload_associated_models
-
- List.preload_preferences_for_user(lists, current_user)
-
- render json: serialize_as_json(lists)
- else
- head :unprocessable_entity
- end
- end
-
- private
-
- def list_creation_attrs
- %i[label_id]
- end
-
- def list_update_attrs
- %i[collapsed position]
- end
-
- def create_list_params
- params.require(:list).permit(list_creation_attrs)
- end
-
- def update_list_params
- params.require(:list).permit(list_update_attrs)
- end
-
- def serialize_as_json(resource)
- resource.as_json(serialization_attrs)
- end
-
- def serialization_attrs
- {
- only: [:id, :list_type, :position],
- methods: [:title],
- label: true,
- collapsed: true,
- current_user: current_user
- }
- end
- end
-end
-
-Boards::ListsController.prepend_mod_with('Boards::ListsController')
diff --git a/app/controllers/concerns/access_tokens_actions.rb b/app/controllers/concerns/access_tokens_actions.rb
index 451841c43bb..6e43be5594d 100644
--- a/app/controllers/concerns/access_tokens_actions.rb
+++ b/app/controllers/concerns/access_tokens_actions.rb
@@ -22,11 +22,10 @@ module AccessTokensActions
if token_response.success?
@resource_access_token = token_response.payload[:access_token]
- PersonalAccessToken.redis_store!(key_identity, @resource_access_token.token)
-
- redirect_to resource_access_tokens_path, notice: _("Your new access token has been created.")
+ render json: { new_token: @resource_access_token.token,
+ active_access_tokens: active_resource_access_tokens }, status: :ok
else
- redirect_to resource_access_tokens_path, alert: _("Failed to create new access token: %{token_response_message}") % { token_response_message: token_response.message }
+ render json: { errors: token_response.errors }, status: :unprocessable_entity
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -63,12 +62,15 @@ module AccessTokensActions
resource.members.load
@scopes = Gitlab::Auth.resource_bot_scopes
- @active_resource_access_tokens = finder(state: 'active').execute.preload_users
- @inactive_resource_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute.preload_users
- @new_resource_access_token = PersonalAccessToken.redis_getdel(key_identity)
+ @active_resource_access_tokens = active_resource_access_tokens
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
+ def active_resource_access_tokens
+ tokens = finder(state: 'active', sort: 'expires_at_asc_id_desc').execute.preload_users
+ represent(tokens)
+ end
+
def finder(options = {})
PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
end
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 4228a93d310..fbaa754124c 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -89,6 +89,7 @@ module AuthenticatesWithTwoFactor
user.save!
sign_in(user, message: :two_factor_authenticated, event: :authentication)
else
+ send_two_factor_otp_attempt_failed_email(user)
handle_two_factor_failure(user, 'OTP', _('Invalid two-factor code.'))
end
end
@@ -158,6 +159,10 @@ module AuthenticatesWithTwoFactor
prompt_for_two_factor(user)
end
+ def send_two_factor_otp_attempt_failed_email(user)
+ user.notification_service.two_factor_otp_attempt_failed(user, request.remote_ip)
+ end
+
def log_failed_two_factor(user, method)
# overridden in EE
end
diff --git a/app/controllers/concerns/boards_actions.rb b/app/controllers/concerns/boards_actions.rb
index 2f9edfad12d..42bf6c68aa7 100644
--- a/app/controllers/concerns/boards_actions.rb
+++ b/app/controllers/concerns/boards_actions.rb
@@ -5,41 +5,38 @@ module BoardsActions
extend ActiveSupport::Concern
included do
- include BoardsResponses
-
before_action :authorize_read_board!, only: [:index, :show]
- before_action :boards, only: :index
- before_action :board, only: :show
+ before_action :redirect_to_recent_board, only: [:index]
+ before_action :board, only: [:index, :show]
before_action :push_licensed_features, only: [:index, :show]
end
def index
- respond_with_boards
+ # if no board exists, create one
+ @board = board_create_service.execute.payload unless board # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def show
- # Add / update the board in the recent visits table
- board_visit_service.new(parent, current_user).execute(board) if request.format.html?
+ return render_404 unless board
- respond_with_board
+ # Add / update the board in the recent visits table
+ board_visit_service.new(parent, current_user).execute(board)
end
private
- # Noop on FOSS
- def push_licensed_features
+ def redirect_to_recent_board
+ return if !parent.multiple_issue_boards_available? || !latest_visited_board
+
+ redirect_to board_path(latest_visited_board.board)
end
- def boards
- strong_memoize(:boards) do
- existing_boards = boards_finder.execute
- if existing_boards.any?
- existing_boards
- else
- # if no board exists, create one
- [board_create_service.execute.payload]
- end
- end
+ def latest_visited_board
+ @latest_visited_board ||= Boards::VisitsFinder.new(parent, current_user).latest
+ end
+
+ # Noop on FOSS
+ def push_licensed_features
end
def board
@@ -48,20 +45,26 @@ module BoardsActions
end
end
- def board_type
- board_klass.to_type
- end
-
def board_visit_service
Boards::Visits::CreateService
end
- def serializer
- BoardSerializer.new(current_user: current_user)
+ def parent
+ strong_memoize(:parent) do
+ group? ? group : project
+ end
+ end
+
+ def board_path(board)
+ if group?
+ group_board_path(parent, board)
+ else
+ project_board_path(parent, board)
+ end
end
- def serialize_as_json(resource)
- serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?)
+ def group?
+ instance_variable_defined?(:@group)
end
end
diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
deleted file mode 100644
index eb7392648a1..00000000000
--- a/app/controllers/concerns/boards_responses.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-# frozen_string_literal: true
-
-module BoardsResponses
- include Gitlab::Utils::StrongMemoize
-
- # Overridden on EE module
- def board_params
- params.require(:board).permit(:name)
- end
-
- def parent
- strong_memoize(:parent) do
- group? ? group : project
- end
- end
-
- def boards_path
- if group?
- group_boards_path(parent)
- else
- project_boards_path(parent)
- end
- end
-
- def board_path(board)
- if group?
- group_board_path(parent, board)
- else
- project_board_path(parent, board)
- end
- end
-
- def group?
- instance_variable_defined?(:@group)
- end
-
- def authorize_read_list
- authorize_action_for!(board, :read_issue_board_list)
- end
-
- def authorize_read_issue
- authorize_action_for!(board, :read_issue)
- end
-
- def authorize_update_issue
- authorize_action_for!(issue, :admin_issue)
- end
-
- def authorize_create_issue
- list = List.find(issue_params[:list_id])
- action = list.backlog? ? :create_issue : :admin_issue
-
- authorize_action_for!(project, action)
- end
-
- def authorize_admin_list
- authorize_action_for!(board, :admin_issue_board_list)
- end
-
- def authorize_action_for!(resource, ability)
- return render_403 unless can?(current_user, ability, resource)
- end
-
- def respond_with_boards
- respond_with(@boards) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- end
-
- def respond_with_board
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
- return render_404 unless @board
-
- respond_with(@board)
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
- end
-
- def serialize_as_json(resource)
- serializer.represent(resource).as_json
- end
-
- def respond_with(resource)
- respond_to do |format|
- format.html
- format.json do
- render json: serialize_as_json(resource)
- end
- end
- end
-
- def serializer
- BoardSerializer.new
- end
-end
-
-BoardsResponses.prepend_mod_with('BoardsResponses')
diff --git a/app/controllers/concerns/import/github_oauth.rb b/app/controllers/concerns/import/github_oauth.rb
new file mode 100644
index 00000000000..d53022aabf2
--- /dev/null
+++ b/app/controllers/concerns/import/github_oauth.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+module Import
+ module GithubOauth
+ extend ActiveSupport::Concern
+
+ OAuthConfigMissingError = Class.new(StandardError)
+
+ included do
+ rescue_from OAuthConfigMissingError, with: :missing_oauth_config
+ end
+
+ private
+
+ def provider_auth
+ return if session[access_token_key].present?
+
+ go_to_provider_for_permissions unless ci_cd_only?
+ end
+
+ def ci_cd_only?
+ %w[1 true].include?(params[:ci_cd_only])
+ end
+
+ def go_to_provider_for_permissions
+ redirect_to authorize_url
+ end
+
+ def oauth_client
+ raise OAuthConfigMissingError unless oauth_config
+
+ oauth_client_from_config
+ end
+
+ def oauth_client_from_config
+ @oauth_client_from_config ||= ::OAuth2::Client.new(
+ oauth_config.app_id,
+ oauth_config.app_secret,
+ oauth_options.merge(ssl: { verify: oauth_config['verify_ssl'] })
+ )
+ end
+
+ def oauth_config
+ @oauth_config ||= Gitlab::Auth::OAuth::Provider.config_for('github')
+ end
+
+ def oauth_options
+ return unless oauth_config
+
+ oauth_config.dig('args', 'client_options').deep_symbolize_keys
+ end
+
+ def authorize_url
+ state = SecureRandom.base64(64)
+ session[auth_state_key] = state
+ if Feature.enabled?(:remove_legacy_github_client)
+ oauth_client.auth_code.authorize_url(
+ redirect_uri: callback_import_url,
+ scope: 'repo, user, user:email',
+ state: state
+ )
+ else
+ client.authorize_url(callback_import_url, state)
+ end
+ end
+
+ def get_token(code)
+ if Feature.enabled?(:remove_legacy_github_client)
+ oauth_client.auth_code.get_token(code).token
+ else
+ client.get_token(code)
+ end
+ end
+
+ def missing_oauth_config
+ session[access_token_key] = nil
+
+ message = _('Missing OAuth configuration for GitHub.')
+
+ respond_to do |format|
+ format.json do
+ render json: { errors: message }, status: :unauthorized
+ end
+
+ format.any do
+ redirect_to new_import_url,
+ alert: message
+ end
+ end
+ end
+
+ def callback_import_url
+ public_send("users_import_#{provider_name}_callback_url", extra_import_params.merge({ namespace_id: params[:namespace_id] })) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def extra_import_params
+ {}
+ end
+ end
+end
diff --git a/app/controllers/concerns/issuable_collections_action.rb b/app/controllers/concerns/issuable_collections_action.rb
index 96cf6021ea9..e03d1de7bf9 100644
--- a/app/controllers/concerns/issuable_collections_action.rb
+++ b/app/controllers/concerns/issuable_collections_action.rb
@@ -59,9 +59,12 @@ module IssuableCollectionsAction
end
def finder_options
+ issue_types = Issue::TYPES_FOR_LIST
+ issue_types = issue_types.excluding('task') unless Feature.enabled?(:work_items)
+
super.merge(
non_archived: true,
- issue_types: Issue::TYPES_FOR_LIST
+ issue_types: issue_types
)
end
end
diff --git a/app/controllers/concerns/multiple_boards_actions.rb b/app/controllers/concerns/multiple_boards_actions.rb
deleted file mode 100644
index 685c93fc2a2..00000000000
--- a/app/controllers/concerns/multiple_boards_actions.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-# frozen_string_literal: true
-
-module MultipleBoardsActions
- include Gitlab::Utils::StrongMemoize
- extend ActiveSupport::Concern
-
- included do
- include BoardsActions
-
- before_action :redirect_to_recent_board, only: [:index]
- before_action :authenticate_user!, only: [:recent]
- before_action :authorize_create_board!, only: [:create]
- before_action :authorize_admin_board!, only: [:create, :update, :destroy]
- end
-
- def recent
- recent_visits = ::Boards::VisitsFinder.new(parent, current_user).latest(Board::RECENT_BOARDS_SIZE)
- recent_boards = recent_visits.map(&:board)
-
- render json: serialize_as_json(recent_boards)
- end
-
- def create
- response = Boards::CreateService.new(parent, current_user, board_params).execute
-
- respond_to do |format|
- format.json do
- board = response.payload
-
- if response.success?
- extra_json = { board_path: board_path(board) }
- render json: serialize_as_json(board).merge(extra_json)
- else
- render json: board.errors, status: :unprocessable_entity
- end
- end
- end
- end
-
- def update
- service = Boards::UpdateService.new(parent, current_user, board_params)
-
- respond_to do |format|
- format.json do
- if service.execute(board)
- extra_json = { board_path: board_path(board) }
- render json: serialize_as_json(board).merge(extra_json)
- else
- render json: board.errors, status: :unprocessable_entity
- end
- end
- end
- end
-
- def destroy
- service = Boards::DestroyService.new(parent, current_user)
- service.execute(board)
-
- respond_to do |format|
- format.json { head :ok }
- format.html { redirect_to boards_path, status: :found }
- end
- end
-
- private
-
- def redirect_to_recent_board
- return unless board_type == Board.to_type
- return if request.format.json? || !parent.multiple_issue_boards_available? || !latest_visited_board
-
- redirect_to board_path(latest_visited_board.board)
- end
-
- def latest_visited_board
- @latest_visited_board ||= Boards::VisitsFinder.new(parent, current_user).latest
- end
-
- def authorize_create_board!
- check_multiple_group_issue_boards_available! if group?
- end
-
- def authorize_admin_board!
- return render_404 unless can?(current_user, :admin_issue_board, parent)
- end
-
- def serializer
- BoardSerializer.new(current_user: current_user)
- end
-
- def serialize_as_json(resource)
- serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?)
- end
-end
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index 1d2f9e31c46..79b3fa28660 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -26,16 +26,24 @@ module PreviewMarkdown
}
end
+ def timeline_events_filter_params
+ {
+ issuable_reference_expansion_enabled: true,
+ pipeline: :'incident_management/timeline_event'
+ }
+ end
+
def markdown_service_params
params
end
def markdown_context_params
case controller_name
- when 'wikis' then { pipeline: :wiki, wiki: wiki, page_slug: params[:id] }
- when 'snippets' then { skip_project_check: true }
- when 'groups' then { group: group }
- when 'projects' then projects_filter_params
+ when 'wikis' then { pipeline: :wiki, wiki: wiki, page_slug: params[:id] }
+ when 'snippets' then { skip_project_check: true }
+ when 'groups' then { group: group }
+ when 'projects' then projects_filter_params
+ when 'timeline_events' then timeline_events_filter_params
else {}
end.merge(requested_path: params[:path], ref: params[:ref])
end
diff --git a/app/controllers/concerns/product_analytics_tracking.rb b/app/controllers/concerns/product_analytics_tracking.rb
index 8e936782e5a..4f96cc5c895 100644
--- a/app/controllers/concerns/product_analytics_tracking.rb
+++ b/app/controllers/concerns/product_analytics_tracking.rb
@@ -29,7 +29,13 @@ module ProductAnalyticsTracking
track_unique_redis_hll_event(name, &block) if destinations.include?(:redis_hll)
if destinations.include?(:snowplow) && event_enabled?(name)
- Gitlab::Tracking.event(self.class.to_s, name, namespace: tracking_namespace_source, user: current_user)
+ Gitlab::Tracking.event(
+ self.class.to_s,
+ name,
+ namespace: tracking_namespace_source,
+ user: current_user,
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: name).to_context]
+ )
end
end
@@ -49,6 +55,7 @@ module ProductAnalyticsTracking
user: current_user,
property: name,
label: label,
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: name).to_context],
**optional_arguments
)
end
diff --git a/app/controllers/concerns/registrations_tracking.rb b/app/controllers/concerns/registrations_tracking.rb
new file mode 100644
index 00000000000..14743349c1a
--- /dev/null
+++ b/app/controllers/concerns/registrations_tracking.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module RegistrationsTracking
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :glm_tracking_params
+ end
+
+ private
+
+ def glm_tracking_params
+ params.permit(:glm_source, :glm_content)
+ end
+end
diff --git a/app/controllers/concerns/sends_blob.rb b/app/controllers/concerns/sends_blob.rb
index 381f2eba352..3cf260c9f1b 100644
--- a/app/controllers/concerns/sends_blob.rb
+++ b/app/controllers/concerns/sends_blob.rb
@@ -27,12 +27,14 @@ module SendsBlob
private
def cached_blob?(blob, allow_caching: false)
- stale = stale?(etag: blob.id) # The #stale? method sets cache headers.
-
- # Because we are opinionated we set the cache headers ourselves.
- response.cache_control[:public] = allow_caching
+ stale =
+ if Feature.enabled?(:improve_blobs_cache_headers)
+ stale?(strong_etag: blob.id)
+ else
+ stale?(etag: blob.id)
+ end
- response.cache_control[:max_age] =
+ max_age =
if @ref && @commit && @ref == @commit.id # rubocop:disable Gitlab/ModuleWithInstanceVariables
# This is a link to a commit by its commit SHA. That means that the blob
# is immutable. The only reason to invalidate the cache is if the commit
@@ -44,6 +46,16 @@ module SendsBlob
Blob::CACHE_TIME
end
+ # Because we are opinionated we set the cache headers ourselves.
+ if Feature.enabled?(:improve_blobs_cache_headers)
+ expires_in(max_age,
+ public: allow_caching, must_revalidate: true, stale_if_error: 5.minutes,
+ stale_while_revalidate: 1.minute, 's-maxage': 1.minute)
+ else
+ response.cache_control[:public] = allow_caching
+ response.cache_control[:max_age] = max_age
+ end
+
!stale
end
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 83447744013..2b781c528ad 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -209,9 +209,7 @@ module WikiActions
def wiki
strong_memoize(:wiki) do
wiki = Wiki.for_container(container, current_user)
-
- # Call #wiki to make sure the Wiki Repo is initialized
- wiki.wiki
+ wiki.create_wiki_repository
wiki
end
@@ -242,7 +240,7 @@ module WikiActions
def wiki_pages
strong_memoize(:wiki_pages) do
Kaminari.paginate_array(
- wiki.list_pages(sort: params[:sort], direction: params[:direction])
+ wiki.list_pages(direction: params[:direction])
).page(params[:page])
end
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index aec3247f4b2..f8cfa996447 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -67,6 +67,10 @@ class Groups::ApplicationController < ApplicationController
end
end
+ def authorize_billings_page!
+ render_404 unless can?(current_user, :read_billing, group)
+ end
+
def authorize_read_group_member!
unless can?(current_user, :read_group_member, group)
render_403
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index e64d838b7d1..14b70df0feb 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -5,7 +5,6 @@ class Groups::BoardsController < Groups::ApplicationController
include RecordUserLastActivity
include Gitlab::Utils::StrongMemoize
- before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:board_multi_select, group)
push_frontend_feature_flag(:realtime_labels, group)
@@ -20,16 +19,6 @@ class Groups::BoardsController < Groups::ApplicationController
private
- def board_klass
- Board
- end
-
- def boards_finder
- strong_memoize :boards_finder do
- Boards::BoardsFinder.new(parent, current_user)
- end
- end
-
def board_finder
strong_memoize :board_finder do
Boards::BoardsFinder.new(parent, current_user, board_id: params[:id])
@@ -42,10 +31,6 @@ class Groups::BoardsController < Groups::ApplicationController
end
end
- def assign_endpoint_vars
- @boards_endpoint = group_boards_path(group)
- end
-
def authorize_read_board!
access_denied! unless can?(current_user, :read_issue_board, group)
end
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index 652f12e34ba..18b055b3f05 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -2,13 +2,9 @@
class Groups::RunnersController < Groups::ApplicationController
before_action :authorize_read_group_runners!, only: [:index, :show]
- before_action :authorize_admin_group_runners!, only: [:edit, :update, :destroy, :pause, :resume]
+ before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume]
before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
- before_action only: [:show] do
- push_frontend_feature_flag(:enforce_runner_token_expires_at)
- end
-
feature_category :runner
urgency :low
@@ -37,7 +33,9 @@ class Groups::RunnersController < Groups::ApplicationController
private
def runner
- @runner ||= Ci::RunnersFinder.new(current_user: current_user, params: { group: @group }).execute
+ group_params = { group: @group, membership: :all_available }
+
+ @runner ||= Ci::RunnersFinder.new(current_user: current_user, params: group_params).execute
.except(:limit, :offset)
.find(params[:id])
end
@@ -45,6 +43,12 @@ class Groups::RunnersController < Groups::ApplicationController
def runner_params
params.require(:runner).permit(Ci::Runner::FORM_EDITABLE)
end
+
+ def authorize_update_runner!
+ return if can?(current_user, :admin_group_runners, group) && can?(current_user, :update_runner, runner)
+
+ render_404
+ end
end
Groups::RunnersController.prepend_mod
diff --git a/app/controllers/groups/settings/access_tokens_controller.rb b/app/controllers/groups/settings/access_tokens_controller.rb
index b9ab2e008cc..f01b2b779e3 100644
--- a/app/controllers/groups/settings/access_tokens_controller.rb
+++ b/app/controllers/groups/settings/access_tokens_controller.rb
@@ -13,6 +13,12 @@ module Groups
def resource_access_tokens_path
group_settings_access_tokens_path
end
+
+ private
+
+ def represent(tokens)
+ ::GroupAccessTokenSerializer.new.represent(tokens, group: resource)
+ end
end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 9316204d89c..269342a6c22 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -81,9 +81,9 @@ class GroupsController < Groups::ApplicationController
successful_creation_hooks
notice = if @group.chat_team.present?
- "Group '#{@group.name}' and its Mattermost team were successfully created."
+ format(_("Group %{group_name} and its Mattermost team were successfully created."), group_name: @group.name)
else
- "Group '#{@group.name}' was successfully created."
+ format(_("Group %{group_name} was successfully created."), group_name: @group.name)
end
redirect_to @group, notice: notice
@@ -393,7 +393,7 @@ class GroupsController < Groups::ApplicationController
end
def captcha_enabled?
- Gitlab::Recaptcha.enabled? && Feature.enabled?(:recaptcha_on_top_level_group_creation, type: :ops)
+ helpers.recaptcha_enabled? && Feature.enabled?(:recaptcha_on_top_level_group_creation, type: :ops)
end
def captcha_required?
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index 58a985cbc46..fcf6871d137 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class IdeController < ApplicationController
- layout 'fullscreen'
-
include ClientsidePreviewCSP
include StaticObjectExternalStorageCSP
include Gitlab::Utils::StrongMemoize
@@ -13,7 +11,6 @@ class IdeController < ApplicationController
push_frontend_feature_flag(:build_service_proxy)
push_frontend_feature_flag(:schema_linting)
push_frontend_feature_flag(:reject_unsigned_commits_by_gitlab)
- push_frontend_feature_flag(:vscode_web_ide, current_user)
define_index_vars
end
@@ -28,6 +25,8 @@ class IdeController < ApplicationController
Gitlab::Tracking.event(self.class.to_s, 'web_ide_views',
namespace: project&.namespace, user: current_user)
end
+
+ render layout: 'fullscreen', locals: { minimal: helpers.use_new_web_ide? }
end
private
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index 893c0b6ac54..655fc7854fe 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -47,6 +47,8 @@ class Import::BulkImportsController < ApplicationController
end
def create
+ return render json: { success: false }, status: :unprocessable_entity unless valid_create_params?
+
responses = create_params.map do |entry|
if entry[:destination_name]
entry[:destination_slug] ||= entry[:destination_name]
@@ -102,6 +104,10 @@ class Import::BulkImportsController < ApplicationController
params.permit(bulk_import: bulk_import_params)[:bulk_import]
end
+ def valid_create_params?
+ create_params.all? { _1[:source_type] == 'group_entity' }
+ end
+
def bulk_import_params
%i[
source_type
@@ -113,7 +119,7 @@ class Import::BulkImportsController < ApplicationController
end
def ensure_group_import_enabled
- render_404 unless Feature.enabled?(:bulk_import)
+ render_404 unless ::BulkImports::Features.enabled?
end
def access_token_key
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 8a3e6809736..92763e09ba3 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -5,14 +5,12 @@ class Import::GithubController < Import::BaseController
include ImportHelper
include ActionView::Helpers::SanitizeHelper
+ include Import::GithubOauth
before_action :verify_import_enabled
before_action :provider_auth, only: [:status, :realtime_changes, :create]
before_action :expire_etag_cache, only: [:status, :create]
- OAuthConfigMissingError = Class.new(StandardError)
-
- rescue_from OAuthConfigMissingError, with: :missing_oauth_config
rescue_from Octokit::Unauthorized, with: :provider_unauthorized
rescue_from Octokit::TooManyRequests, with: :provider_rate_limit
rescue_from Gitlab::GithubImport::RateLimitError, with: :rate_limit_threshold_exceeded
@@ -73,6 +71,17 @@ class Import::GithubController < Import::BaseController
}
end
+ def cancel
+ project = Project.imported_from(provider_name).find(params[:project_id])
+ result = Import::Github::CancelProjectImportService.new(project, current_user).execute
+
+ if result[:status] == :success
+ render json: serialized_imported_projects(result[:project])
+ else
+ render json: { errors: result[:message] }, status: result[:http_status]
+ end
+ end
+
protected
override :importable_repos
@@ -104,7 +113,7 @@ class Import::GithubController < Import::BaseController
end
def permitted_import_params
- [:repo_id, :new_name, :target_namespace]
+ [:repo_id, :new_name, :target_namespace, { optional_stages: {} }]
end
def serialized_imported_projects(projects = already_added_projects)
@@ -143,58 +152,10 @@ class Import::GithubController < Import::BaseController
@filter = @filter&.tr(' ', '')&.tr(':', '')
end
- def oauth_client
- raise OAuthConfigMissingError unless oauth_config
-
- @oauth_client ||= ::OAuth2::Client.new(
- oauth_config.app_id,
- oauth_config.app_secret,
- oauth_options.merge(ssl: { verify: oauth_config['verify_ssl'] })
- )
- end
-
- def oauth_config
- @oauth_config ||= Gitlab::Auth::OAuth::Provider.config_for('github')
- end
-
- def oauth_options
- if oauth_config
- oauth_config.dig('args', 'client_options').deep_symbolize_keys
- else
- OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
- end
- end
-
- def authorize_url
- state = SecureRandom.base64(64)
- session[auth_state_key] = state
- if Feature.enabled?(:remove_legacy_github_client)
- oauth_client.auth_code.authorize_url(
- redirect_uri: callback_import_url,
- scope: 'repo, user, user:email',
- state: state
- )
- else
- client.authorize_url(callback_import_url, state)
- end
- end
-
- def get_token(code)
- if Feature.enabled?(:remove_legacy_github_client)
- oauth_client.auth_code.get_token(code).token
- else
- client.get_token(code)
- end
- end
-
def verify_import_enabled
render_404 unless import_enabled?
end
- def go_to_provider_for_permissions
- redirect_to authorize_url
- end
-
def import_enabled?
__send__("#{provider_name}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend
end
@@ -211,10 +172,6 @@ class Import::GithubController < Import::BaseController
public_send("status_import_#{provider_name}_url", extra_import_params.merge({ namespace_id: params[:namespace_id].presence })) # rubocop:disable GitlabSecurity/PublicSend
end
- def callback_import_url
- public_send("users_import_#{provider_name}_callback_url", extra_import_params.merge({ namespace_id: params[:namespace_id] })) # rubocop:disable GitlabSecurity/PublicSend
- end
-
def provider_unauthorized
session[access_token_key] = nil
redirect_to new_import_url,
@@ -228,12 +185,6 @@ class Import::GithubController < Import::BaseController
alert: _("GitHub API rate limit exceeded. Try again after %{reset_time}") % { reset_time: reset_time }
end
- def missing_oauth_config
- session[access_token_key] = nil
- redirect_to new_import_url,
- alert: _('Missing OAuth configuration for GitHub.')
- end
-
def auth_state_key
:"#{provider_name}_auth_state_key"
end
@@ -252,24 +203,10 @@ class Import::GithubController < Import::BaseController
end
# rubocop: enable CodeReuse/ActiveRecord
- def provider_auth
- if !ci_cd_only? && session[access_token_key].blank?
- go_to_provider_for_permissions
- end
- end
-
- def ci_cd_only?
- %w[1 true].include?(params[:ci_cd_only])
- end
-
def client_options
{ wait_for_rate_limit_reset: false }
end
- def extra_import_params
- {}
- end
-
def rate_limit_threshold_exceeded
head :too_many_requests
end
diff --git a/app/controllers/import/github_groups_controller.rb b/app/controllers/import/github_groups_controller.rb
new file mode 100644
index 00000000000..6c0773bcfb3
--- /dev/null
+++ b/app/controllers/import/github_groups_controller.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Import
+ class GithubGroupsController < ApplicationController
+ include Import::GithubOauth
+
+ before_action :provider_auth, only: [:status]
+ feature_category :importers
+
+ PAGE_LENGTH = 25
+
+ def status
+ respond_to do |format|
+ format.json do
+ render json: { provider_groups: serialized_provider_groups }
+ end
+ end
+ end
+
+ private
+
+ def serialized_provider_groups
+ Import::GithubOrgSerializer.new.represent(importable_orgs)
+ end
+
+ def importable_orgs
+ client_orgs.to_a
+ end
+
+ def client_orgs
+ @client_orgs ||= client.octokit.organizations(nil, pagination_options)
+ end
+
+ def client
+ @client ||= Gitlab::GithubImport::Client.new(session[access_token_key])
+ end
+
+ def pagination_options
+ {
+ page: [1, params[:page].to_i].max,
+ per_page: PAGE_LENGTH
+ }
+ end
+
+ def auth_state_key
+ :"#{provider_name}_auth_state_key"
+ end
+
+ def access_token_key
+ :"#{provider_name}_access_token"
+ end
+
+ def provider_name
+ :github
+ end
+ end
+end
diff --git a/app/controllers/jira_connect/public_keys_controller.rb b/app/controllers/jira_connect/public_keys_controller.rb
new file mode 100644
index 00000000000..b3144993edb
--- /dev/null
+++ b/app/controllers/jira_connect/public_keys_controller.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module JiraConnect
+ class PublicKeysController < ::ApplicationController
+ # This is not inheriting from JiraConnect::Application controller because
+ # it doesn't need to handle JWT authentication.
+
+ feature_category :integrations
+
+ skip_before_action :authenticate_user!
+
+ def show
+ return render_404 if Feature.disabled?(:jira_connect_oauth_self_managed) || !Gitlab.com?
+
+ render plain: public_key.key
+ end
+
+ private
+
+ def public_key
+ JiraConnect::PublicKey.find(params[:id])
+ end
+ end
+end
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index ff466fd5fbb..3b78b997da1 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -4,7 +4,6 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::GonHelper
include PageLayoutHelper
include OauthApplications
- include Gitlab::Experimentation::ControllerConcern
include InitializesCurrentUserMode
# Defined by the `Doorkeeper::ApplicationsController` and is redundant as we call `authenticate_user!` below. Not
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index 2e9fbb1d0d9..bf8b61db2e5 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
- include Gitlab::Experimentation::ControllerConcern
include InitializesCurrentUserMode
include Gitlab::Utils::StrongMemoize
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 817f272d458..f3f0ddd968a 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -181,6 +181,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
accept_pending_invitations(user: user) if new_user
+ persist_accepted_terms_if_required(user) if new_user
+
store_after_sign_up_path_for_user if intent_to_register?
sign_in_and_redirect(user, event: :authentication)
end
@@ -301,6 +303,15 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to new_admin_session_path, alert: _('Invalid login or password')
end
+ def persist_accepted_terms_if_required(user)
+ return unless Feature.enabled?(:update_oauth_registration_flow)
+ return unless user.persisted?
+ return unless Gitlab::CurrentSettings.current_application_settings.enforce_terms?
+
+ terms = ApplicationSetting::Term.latest
+ Users::RespondToTermsService.new(user, terms).execute(accepted: true)
+ end
+
def store_after_sign_up_path_for_user
store_location_for(:user, users_sign_up_welcome_path)
end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 8ed67c26f19..4cf26d3e1e2 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -3,6 +3,8 @@
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
feature_category :authentication_and_authorization
+ before_action :check_personal_access_tokens_enabled
+
def index
set_index_vars
scopes = params[:scopes].split(',').map(&:squish).select(&:present?).map(&:to_sym) unless params[:scopes].nil?
@@ -83,4 +85,8 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def page
(params[:page] || 1).to_i
end
+
+ def check_personal_access_tokens_enabled
+ render_404 if Gitlab::CurrentSettings.personal_access_tokens_disabled?
+ end
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 7aca76c2fb1..a57c87bf691 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -55,7 +55,9 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:sourcegraph_enabled,
:gitpod_enabled,
:render_whitespace_in_code,
- :markdown_surround_selection
+ :markdown_surround_selection,
+ :markdown_automatic_lists,
+ :use_legacy_web_ide
]
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 0b7d4626c6d..0933f2bb7ea 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -15,31 +15,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
feature_category :authentication_and_authorization
def show
- if two_factor_authentication_required? && !current_user.two_factor_enabled?
- two_factor_authentication_reason(
- global: lambda do
- flash.now[:alert] =
- _('The global settings require you to enable Two-Factor Authentication for your account.')
- end,
- group: lambda do |groups|
- flash.now[:alert] = groups_notification(groups)
- end
- )
-
- unless two_factor_grace_period_expired?
- grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
- flash.now[:alert] = flash.now[:alert] + _(" You need to do this before %{grace_period_deadline}.") % { grace_period_deadline: l(grace_period_deadline) }
- end
- end
-
- @qr_code = build_qr_code
- @account_string = account_string
-
- if Feature.enabled?(:webauthn)
- setup_webauthn_registration
- else
- setup_u2f_registration
- end
+ setup_show_page
end
def create
@@ -147,7 +123,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.increment_failed_attempts!
- redirect_to profile_two_factor_auth_path, alert: _('You must provide a valid current password')
+ @error = { message: _('You must provide a valid current password') }
+
+ setup_show_page
+
+ render 'show'
end
def current_password_required?
@@ -245,4 +225,32 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
redirect_to profile_emails_path, notice: s_('You need to verify your primary email first before enabling Two-Factor Authentication.')
end
end
+
+ def setup_show_page
+ if two_factor_authentication_required? && !current_user.two_factor_enabled?
+ two_factor_authentication_reason(
+ global: lambda do
+ flash.now[:alert] =
+ _('The global settings require you to enable Two-Factor Authentication for your account.')
+ end,
+ group: lambda do |groups|
+ flash.now[:alert] = groups_notification(groups)
+ end
+ )
+
+ unless two_factor_grace_period_expired?
+ grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
+ flash.now[:alert] = flash.now[:alert] + _(" You need to do this before %{grace_period_deadline}.") % { grace_period_deadline: l(grace_period_deadline) }
+ end
+ end
+
+ @qr_code = build_qr_code
+ @account_string = account_string
+
+ if Feature.enabled?(:webauthn)
+ setup_webauthn_registration
+ else
+ setup_u2f_registration
+ end
+ end
end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 028b7af02c9..2256471047d 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -39,8 +39,8 @@ class Projects::ApplicationController < ApplicationController
access_denied!(
_('You must have developer or higher permissions in the associated project to view job logs when debug trace ' \
"is enabled. To disable debug trace, set the 'CI_DEBUG_TRACE' variable to 'false' in your pipeline " \
- 'configuration or CI/CD settings. If you need to view this job log, a project maintainer must add you to ' \
- 'the project with developer permissions or higher.')
+ 'configuration or CI/CD settings. If you need to view this job log, a project maintainer or owner must add ' \
+ 'you to the project with developer permissions or higher.')
)
else
access_denied!(_('The current user is not authorized to access the job log.'))
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 9dbf989ca3f..7755effe1da 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -41,7 +41,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end
def contacts
- render json: autocomplete_service.contacts
+ render json: autocomplete_service.contacts(target)
end
private
@@ -51,9 +51,12 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end
def target
+ # type_id is not required in general
+ target_type = params.require(:type)
+
QuickActions::TargetService
.new(project, current_user)
- .execute(params[:type], params[:type_id])
+ .execute(target_type, params[:type_id])
end
def authorize_read_crm_contact!
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 2a20c67a23d..01ed5473b41 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -26,7 +26,10 @@ class Projects::BlameController < Projects::ApplicationController
blame_service = Projects::BlameService.new(@blob, @commit, params.permit(:page, :no_pagination))
@blame = Gitlab::View::Presenter::Factory.new(blame_service.blame, project: @project, path: @path, page: blame_service.page).fabricate!
+
@blame_pagination = blame_service.pagination
+
+ @blame_per_page = blame_service.per_page
end
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 82b35a22669..6a6701ead15 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
class Projects::BoardsController < Projects::ApplicationController
- include MultipleBoardsActions
+ include BoardsActions
include IssuableCollections
before_action :check_issues_available!
- before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:board_multi_select, project)
push_frontend_feature_flag(:realtime_labels, project&.group)
@@ -20,16 +19,6 @@ class Projects::BoardsController < Projects::ApplicationController
private
- def board_klass
- Board
- end
-
- def boards_finder
- strong_memoize :boards_finder do
- Boards::BoardsFinder.new(parent, current_user)
- end
- end
-
def board_finder
strong_memoize :board_finder do
Boards::BoardsFinder.new(parent, current_user, board_id: params[:id])
@@ -42,11 +31,6 @@ class Projects::BoardsController < Projects::ApplicationController
end
end
- def assign_endpoint_vars
- @boards_endpoint = project_boards_path(project)
- @bulk_issues_path = bulk_update_project_issues_path(project)
- end
-
def authorize_read_board!
access_denied! unless can?(current_user, :read_issue_board, project)
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index d7fd65f02a8..61308f24412 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -47,7 +47,8 @@ class Projects::CompareController < Projects::ApplicationController
from_to_vars = {
from: compare_params[:from].presence,
to: compare_params[:to].presence,
- from_project_id: compare_params[:from_project_id].presence
+ from_project_id: compare_params[:from_project_id].presence,
+ straight: compare_params[:straight].presence
}
if from_to_vars[:from].blank? || from_to_vars[:to].blank?
@@ -112,7 +113,11 @@ class Projects::CompareController < Projects::ApplicationController
def compare
return @compare if defined?(@compare)
- @compare = CompareService.new(source_project, head_ref).execute(target_project, start_ref)
+ @compare = CompareService.new(source_project, head_ref).execute(target_project, start_ref, straight: straight)
+ end
+
+ def straight
+ compare_params[:straight] == "true"
end
def start_ref
@@ -160,6 +165,6 @@ class Projects::CompareController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def compare_params
- @compare_params ||= params.permit(:from, :to, :from_project_id)
+ @compare_params ||= params.permit(:from, :to, :from_project_id, :straight)
end
end
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 96afe9dbb9f..22a42d22914 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -27,11 +27,9 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
- @key = DeployKeys::CreateService.new(current_user, create_params).execute(project: @project)
+ @key = DeployKeys::CreateService.new(current_user, create_params).execute(project: @project).present
- unless @key.valid?
- flash[:alert] = @key.errors.full_messages.join(', ').html_safe
- end
+ flash[:alert] = @key.humanized_error_message unless @key.valid?
redirect_to_repository
end
diff --git a/app/controllers/projects/google_cloud/databases_controller.rb b/app/controllers/projects/google_cloud/databases_controller.rb
index 8f7554f248b..77ee830fd24 100644
--- a/app/controllers/projects/google_cloud/databases_controller.rb
+++ b/app/controllers/projects/google_cloud/databases_controller.rb
@@ -50,16 +50,15 @@ module Projects
track_event(:error_enable_cloudsql_services)
flash[:error] = error_message(enable_response[:message])
else
- permitted_params = params.permit(:gcp_project, :ref, :database_version, :tier)
create_response = ::GoogleCloud::CreateCloudsqlInstanceService
- .new(project, current_user, create_service_params(permitted_params))
+ .new(project, current_user, create_service_params)
.execute
if create_response[:status] == :error
track_event(:error_create_cloudsql_instance)
flash[:warning] = error_message(create_response[:message])
else
- track_event(:create_cloudsql_instance, permitted_params.to_s)
+ track_event(:create_cloudsql_instance, permitted_params_create.to_s)
flash[:notice] = success_message
end
end
@@ -69,17 +68,25 @@ module Projects
private
+ def permitted_params_create
+ params.permit(:gcp_project, :ref, :database_version, :tier)
+ end
+
def enable_service_params
- { google_oauth2_token: token_in_session }
+ {
+ google_oauth2_token: token_in_session,
+ gcp_project_id: permitted_params_create[:gcp_project],
+ environment_name: permitted_params_create[:ref]
+ }
end
- def create_service_params(permitted_params)
+ def create_service_params
{
google_oauth2_token: token_in_session,
- gcp_project_id: permitted_params[:gcp_project],
- environment_name: permitted_params[:ref],
- database_version: permitted_params[:database_version],
- tier: permitted_params[:tier]
+ gcp_project_id: permitted_params_create[:gcp_project],
+ environment_name: permitted_params_create[:ref],
+ database_version: permitted_params_create[:database_version],
+ tier: permitted_params_create[:tier]
}
end
diff --git a/app/controllers/projects/incident_management/timeline_events_controller.rb b/app/controllers/projects/incident_management/timeline_events_controller.rb
new file mode 100644
index 00000000000..7e7a4758e48
--- /dev/null
+++ b/app/controllers/projects/incident_management/timeline_events_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Projects
+ module IncidentManagement
+ class TimelineEventsController < Projects::ApplicationController
+ include PreviewMarkdown
+
+ before_action :authenticate_user!
+
+ respond_to :json
+
+ feature_category :incident_management
+ urgency :low
+ end
+ end
+end
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index cbf0c756e1e..089ee860ea6 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -7,11 +7,9 @@ class Projects::IncidentsController < Projects::ApplicationController
before_action :authorize_read_issue!
before_action :load_incident, only: [:show]
before_action do
- push_frontend_feature_flag(:incident_timeline, @project)
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:work_items_hierarchy, @project)
- push_frontend_feature_flag(:remove_user_attributes_projects, @project)
end
feature_category :incident_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 800a7df2566..5b1117c0224 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -41,8 +41,8 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_download_code!, only: [:related_branches]
before_action do
- push_frontend_feature_flag(:incident_timeline, project)
- push_frontend_feature_flag(:remove_user_attributes_projects, project)
+ push_frontend_feature_flag(:preserve_unchanged_markdown, project)
+ push_frontend_feature_flag(:content_editor_on_issues, project)
end
before_action only: [:index, :show] do
@@ -147,19 +147,26 @@ class Projects::IssuesController < Projects::ApplicationController
spam_params = ::Spam::SpamParams.new_from_request(request: request)
service = ::Issues::CreateService.new(project: project, current_user: current_user, params: create_params, spam_params: spam_params)
- @issue = service.execute
+ result = service.execute
- create_vulnerability_issue_feedback(issue)
-
- if service.discussions_to_resolve.count(&:resolved?) > 0
- flash[:notice] = if service.discussion_to_resolve_id
- _("Resolved 1 discussion.")
- else
- _("Resolved all discussions.")
- end
+ # Only irrecoverable errors such as unauthorized user won't contain an issue in the response
+ if result.error? && result[:issue].blank?
+ render_by_create_result_error(result) && return
end
- if @issue.valid?
+ @issue = result[:issue]
+
+ if result.success?
+ create_vulnerability_issue_feedback(@issue)
+
+ if service.discussions_to_resolve.count(&:resolved?) > 0
+ flash[:notice] = if service.discussion_to_resolve_id
+ _("Resolved 1 discussion.")
+ else
+ _("Resolved all discussions.")
+ end
+ end
+
redirect_to project_issue_path(@project, @issue)
else
# NOTE: this CAPTCHA support method is indirectly included via IssuableActions
@@ -372,6 +379,21 @@ class Projects::IssuesController < Projects::ApplicationController
private
+ def render_by_create_result_error(result)
+ Gitlab::AppLogger.warn(
+ message: 'Cannot create issue',
+ errors: result.errors,
+ http_status: result.http_status
+ )
+ error_method_name = "render_#{result.http_status}".to_sym
+
+ if respond_to?(error_method_name, true)
+ send(error_method_name) # rubocop:disable GitlabSecurity/PublicSend
+ else
+ render_404
+ end
+ end
+
def clean_params(all_params)
issue_type = all_params[:issue_type].to_s
all_params.delete(:issue_type) unless WorkItems::Type.allowed_types_for_issues.include?(issue_type)
@@ -383,6 +405,7 @@ class Projects::IssuesController < Projects::ApplicationController
options = super
options[:issue_types] = Issue::TYPES_FOR_LIST
+ options[:issue_types] = options[:issue_types].excluding('task') unless project.work_items_feature_flag_enabled?
if service_desk?
options.reject! { |key| key == 'author_username' || key == 'author_id' }
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index a68c2ffa06d..418e7233e21 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -4,6 +4,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
include DiffHelper
include RendersNotes
include Gitlab::Cache::Helpers
+ include Gitlab::Tracking::Helpers
before_action :commit
before_action :define_diff_vars
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 5a212e9a152..9c139733248 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -34,7 +34,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action only: [:show] do
push_frontend_feature_flag(:merge_request_widget_graphql, project)
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
- push_frontend_feature_flag(:refactor_mr_widgets_extensions, project)
push_frontend_feature_flag(:refactor_code_quality_extension, project)
push_frontend_feature_flag(:refactor_mr_widget_test_summary, project)
push_frontend_feature_flag(:issue_assignees_widget, @project)
@@ -45,7 +44,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:paginated_mr_discussions, project)
push_frontend_feature_flag(:mr_review_submit_comment, project)
push_frontend_feature_flag(:mr_experience_survey, project)
- push_frontend_feature_flag(:remove_user_attributes_projects, @project)
end
before_action do
@@ -451,15 +449,16 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
return :failed
end
+ squashing = params.fetch(:squash, false)
merge_service = ::MergeRequests::MergeService.new(project: @project, current_user: current_user, params: merge_params)
- unless merge_service.hooks_validation_pass?(@merge_request)
+ unless merge_service.hooks_validation_pass?(@merge_request, validate_squash_message: squashing)
return :hook_validation_error
end
return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha
- @merge_request.update(merge_error: nil, squash: params.fetch(:squash, false))
+ @merge_request.update(merge_error: nil, squash: squashing)
if auto_merge_requested?
if merge_request.auto_merge_enabled?
@@ -555,7 +554,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def endpoint_metadata_url(project, merge_request)
- params = request.query_parameters.merge(view: 'inline', diff_head: true)
+ params = request.query_parameters.merge(view: 'inline', diff_head: true, w: current_user&.show_whitespace_in_diffs ? '0' : '1')
diffs_metadata_project_json_merge_request_path(project, merge_request, 'json', params)
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index cfb67b7b4ff..78108cf3478 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -4,8 +4,11 @@ class Projects::MilestonesController < Projects::ApplicationController
include Gitlab::Utils::StrongMemoize
include MilestoneActions
+ REDIRECT_TARGETS = [:new_release].freeze
+
before_action :check_issuables_available!
before_action :milestone, only: [:edit, :update, :destroy, :show, :issues, :merge_requests, :participants, :labels, :promote]
+ before_action :redirect_path, only: [:new, :create]
# Allow read any milestone
before_action :authorize_read_milestone!
@@ -59,7 +62,11 @@ class Projects::MilestonesController < Projects::ApplicationController
@milestone = Milestones::CreateService.new(project, current_user, milestone_params).execute
if @milestone.valid?
- redirect_to project_milestone_path(@project, @milestone)
+ if @redirect_path == :new_release
+ redirect_to new_project_release_path(@project)
+ else
+ redirect_to project_milestone_path(@project, @milestone)
+ end
else
render "new"
end
@@ -113,6 +120,11 @@ class Projects::MilestonesController < Projects::ApplicationController
protected
+ def redirect_path
+ path = params[:redirect_path]&.to_sym
+ @redirect_path = path if REDIRECT_TARGETS.include?(path)
+ end
+
def project_group
strong_memoize(:project_group) do
project.group
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
index a6b22a28b17..43952a2efe4 100644
--- a/app/controllers/projects/pages_domains_controller.rb
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -41,9 +41,9 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end
def create
- @domain = @project.pages_domains.create(create_params)
+ @domain = PagesDomains::CreateService.new(@project, current_user, create_params).execute
- if @domain.valid?
+ if @domain&.persisted?
redirect_to project_pages_domain_path(@project, @domain)
else
render 'new'
@@ -51,7 +51,9 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end
def update
- if @domain.update(update_params)
+ service = ::PagesDomains::UpdateService.new(@project, current_user, update_params)
+
+ if service.execute(@domain)
redirect_to project_pages_path(@project),
status: :found,
notice: 'Domain was updated'
@@ -61,7 +63,9 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end
def destroy
- @domain.destroy
+ PagesDomains::DeleteService
+ .new(@project, current_user)
+ .execute(@domain)
respond_to do |format|
format.html do
@@ -74,9 +78,10 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end
def clean_certificate
- unless @domain.update(user_provided_certificate: nil, user_provided_key: nil)
- flash[:alert] = @domain.errors.full_messages.join(', ')
- end
+ update_params = { user_provided_certificate: nil, user_provided_key: nil }
+ service = ::PagesDomains::UpdateService.new(@project, current_user, update_params)
+
+ flash[:alert] = @domain.errors.full_messages.join(', ') unless service.execute(@domain)
redirect_to project_pages_domain_path(@project, @domain)
end
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index a23d7fb3e6b..ca787785901 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -10,6 +10,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
before_action :authorize_update_pipeline_schedule!, only: [:edit, :update]
before_action :authorize_take_ownership_pipeline_schedule!, only: [:take_ownership]
before_action :authorize_admin_pipeline_schedule!, only: [:destroy]
+ before_action :push_schedule_feature_flag, only: [:index, :new, :edit]
feature_category :continuous_integration
urgency :low
@@ -115,4 +116,8 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def authorize_admin_pipeline_schedule!
return access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule)
end
+
+ def push_schedule_feature_flag
+ push_frontend_feature_flag(:pipeline_schedules_vue, @project)
+ end
end
diff --git a/app/controllers/projects/product_analytics_controller.rb b/app/controllers/projects/product_analytics_controller.rb
index c89cd52530a..8085b0a6334 100644
--- a/app/controllers/projects/product_analytics_controller.rb
+++ b/app/controllers/projects/product_analytics_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Projects::ProductAnalyticsController < Projects::ApplicationController
- before_action :feature_enabled!
+ before_action :feature_enabled!, only: [:index, :setup, :test, :graphs]
before_action :authorize_read_product_analytics!
before_action :tracker_variables, only: [:setup, :test]
@@ -57,3 +57,5 @@ class Projects::ProductAnalyticsController < Projects::ApplicationController
render_404 unless Feature.enabled?(:product_analytics, @project)
end
end
+
+Projects::ProductAnalyticsController.prepend_mod_with('Projects::ProductAnalyticsController')
diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb
index abbfe9ce22a..69a540158c6 100644
--- a/app/controllers/projects/protected_refs_controller.rb
+++ b/app/controllers/projects/protected_refs_controller.rb
@@ -4,7 +4,6 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
include RepositorySettingsRedirect
# Authorize
- before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_ref, only: [:show, :update, :destroy]
diff --git a/app/controllers/projects/settings/access_tokens_controller.rb b/app/controllers/projects/settings/access_tokens_controller.rb
index 32916831ecd..bac35583a97 100644
--- a/app/controllers/projects/settings/access_tokens_controller.rb
+++ b/app/controllers/projects/settings/access_tokens_controller.rb
@@ -13,6 +13,12 @@ module Projects
def resource_access_tokens_path
namespace_project_settings_access_tokens_path
end
+
+ private
+
+ def represent(tokens)
+ ::ProjectAccessTokenSerializer.new.represent(tokens, project: resource)
+ end
end
end
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index c861b24d9ec..76e2da6eb57 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -14,7 +14,7 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
before_action :authorize_read_snippet!, except: [:new, :index]
before_action :authorize_update_snippet!, only: :edit
- urgency :low, [:index]
+ urgency :low, [:index, :show]
def index
@snippet_counts = ::Snippets::CountService
diff --git a/app/controllers/projects/web_ide_terminals_controller.rb b/app/controllers/projects/web_ide_terminals_controller.rb
index 350b091edfa..cfccc949244 100644
--- a/app/controllers/projects/web_ide_terminals_controller.rb
+++ b/app/controllers/projects/web_ide_terminals_controller.rb
@@ -10,6 +10,8 @@ class Projects::WebIdeTerminalsController < Projects::ApplicationController
feature_category :web_ide
+ urgency :low, [:check_config]
+
def check_config
return respond_422 unless branch_sha
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 5ceedbc1e01..b7b6e6534fb 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -311,8 +311,6 @@ class ProjectsController < Projects::ApplicationController
find_tags = true
find_commits = true
- use_gitaly_pagination = Feature.enabled?(:use_gitaly_pagination_for_refs, @project)
-
unless find_refs.nil?
find_branches = find_refs.include?('branches')
find_tags = find_refs.include?('tags')
@@ -323,7 +321,7 @@ class ProjectsController < Projects::ApplicationController
if find_branches
branches = BranchesFinder.new(@repository, refs_params.merge(per_page: REFS_LIMIT))
- .execute(gitaly_pagination: use_gitaly_pagination)
+ .execute(gitaly_pagination: true)
.take(REFS_LIMIT)
.map(&:name)
@@ -332,7 +330,7 @@ class ProjectsController < Projects::ApplicationController
if find_tags && @repository.tag_count.nonzero?
tags = TagsFinder.new(@repository, refs_params.merge(per_page: REFS_LIMIT))
- .execute(gitaly_pagination: use_gitaly_pagination)
+ .execute(gitaly_pagination: true)
.take(REFS_LIMIT)
.map(&:name)
@@ -435,14 +433,14 @@ class ProjectsController < Projects::ApplicationController
analytics_access_level
security_and_compliance_access_level
container_registry_access_level
+ releases_access_level
] + operations_feature_attributes
end
def operations_feature_attributes
if Feature.enabled?(:split_operations_visibility_permissions, project)
%i[
- environments_access_level feature_flags_access_level releases_access_level
- monitor_access_level
+ environments_access_level feature_flags_access_level monitor_access_level
]
else
%i[operations_access_level]
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index 4e18e6a3b20..a49b82319da 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -4,6 +4,7 @@ module Registrations
class WelcomeController < ApplicationController
include OneTrustCSP
include GoogleAnalyticsCSP
+ include RegistrationsTracking
layout 'minimal'
skip_before_action :authenticate_user!, :required_signup_info, :check_two_factor_requirement, only: [:show, :update]
@@ -25,7 +26,7 @@ module Registrations
members = current_user.members
- if members.count == 1 && members.last.source.present?
+ if registering_from_invite?(members)
redirect_to members_activity_path(members), notice: helpers.invite_accepted_notice(members.last)
else
redirect_to path_for_signed_in_user(current_user)
@@ -37,6 +38,10 @@ module Registrations
private
+ def registering_from_invite?(members)
+ members.count == 1 && members.last.source.present?
+ end
+
def require_current_user
return redirect_to new_user_registration_path unless current_user
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 0bd266bb490..31fe30f3f06 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -8,6 +8,7 @@ class RegistrationsController < Devise::RegistrationsController
include OneTrustCSP
include BizibleCSP
include GoogleAnalyticsCSP
+ include RegistrationsTracking
layout 'devise'
@@ -114,13 +115,18 @@ class RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(user)
Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?))
- users_sign_up_welcome_path
+ users_sign_up_welcome_path(glm_tracking_params)
end
def after_inactive_sign_up_path_for(resource)
Gitlab::AppLogger.info(user_created_message)
return new_user_session_path(anchor: 'login-pane') if resource.blocked_pending_approval?
return dashboard_projects_path if Feature.enabled?(:soft_email_confirmation)
+
+ # when email confirmation is enabled, path to redirect is saved
+ # after user confirms and comes back, he will be redirected
+ store_location_for(:redirect, users_sign_up_welcome_path(glm_tracking_params))
+
return identity_verification_redirect_path if custom_confirmation_enabled?(resource)
users_almost_there_path(email: resource.email)
@@ -183,7 +189,7 @@ class RegistrationsController < Devise::RegistrationsController
def resource
@resource ||= Users::RegistrationsBuildService
- .new(current_user, sign_up_params.merge({ skip_confirmation: skip_email_confirmation? }))
+ .new(current_user, sign_up_params.merge({ skip_confirmation: registered_with_invite_email? }))
.execute
end
@@ -191,7 +197,7 @@ class RegistrationsController < Devise::RegistrationsController
@devise_mapping ||= Devise.mappings[:user]
end
- def skip_email_confirmation?
+ def registered_with_invite_email?
invite_email = session.delete(:invite_email)
sign_up_params[:email] == invite_email
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 9f87ad6aaf6..7d4dd04c6d4 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -25,6 +25,10 @@ class SearchController < ApplicationController
end
before_action :check_search_rate_limit!, only: search_rate_limited_endpoints
+ before_action only: :show do
+ push_frontend_feature_flag(:search_page_vertical_nav, current_user)
+ end
+
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
layout 'search'
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index fe3b8d9b8b4..5c969c437f4 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -107,11 +107,11 @@ class SessionsController < Devise::SessionsController
end
def captcha_enabled?
- request.headers[CAPTCHA_HEADER] && Gitlab::Recaptcha.enabled?
+ request.headers[CAPTCHA_HEADER] && helpers.recaptcha_enabled?
end
def captcha_on_login_required?
- Gitlab::Recaptcha.enabled_on_login? && unverified_anonymous_user?
+ helpers.recaptcha_enabled_on_login? && unverified_anonymous_user?
end
# From https://github.com/plataformatec/devise/wiki/How-To:-Use-Recaptcha-with-Devise#devisepasswordscontroller
diff --git a/app/controllers/users/namespace_callouts_controller.rb b/app/controllers/users/namespace_callouts_controller.rb
deleted file mode 100644
index d4876382dfe..00000000000
--- a/app/controllers/users/namespace_callouts_controller.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Users
- class NamespaceCalloutsController < Users::CalloutsController
- private
-
- def callout
- Users::DismissNamespaceCalloutService.new(
- container: nil, current_user: current_user, params: callout_params
- ).execute
- end
-
- def callout_params
- params.permit(:namespace_id).merge(feature_name: feature_name)
- end
- end
-end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 3c1a3534912..c35aa8e4346 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -35,7 +35,7 @@ class UsersController < ApplicationController
feature_category :source_code_management, [:gpg_keys]
# TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/357914
- urgency :low, [:show, :calendar_activities, :contributed, :activity, :projects, :groups, :calendar]
+ urgency :low, [:show, :calendar_activities, :contributed, :activity, :projects, :groups, :calendar, :snippets]
urgency :default, [:followers, :following, :starred]
urgency :high, [:exists]
@@ -174,8 +174,9 @@ class UsersController < ApplicationController
end
def follow
- current_user.follow(user)
+ followee = current_user.follow(user)
+ flash[:alert] = followee.errors.full_messages.join(', ') if followee&.errors&.any?
redirect_path = referer_path(request) || @user
redirect_to redirect_path
diff --git a/app/events/pages_domains/pages_domain_created_event.rb b/app/events/pages_domains/pages_domain_created_event.rb
new file mode 100644
index 00000000000..a86718f4681
--- /dev/null
+++ b/app/events/pages_domains/pages_domain_created_event.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module PagesDomains
+ class PagesDomainCreatedEvent < ::Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'project_id' => { 'type' => 'integer' },
+ 'namespace_id' => { 'type' => 'integer' },
+ 'root_namespace_id' => { 'type' => 'integer' },
+ 'domain' => { 'type' => 'string' }
+ },
+ 'required' => %w[project_id namespace_id root_namespace_id]
+ }
+ end
+ end
+end
diff --git a/app/events/pages_domains/pages_domain_deleted_event.rb b/app/events/pages_domains/pages_domain_deleted_event.rb
new file mode 100644
index 00000000000..7fe165a7249
--- /dev/null
+++ b/app/events/pages_domains/pages_domain_deleted_event.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module PagesDomains
+ class PagesDomainDeletedEvent < ::Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'project_id' => { 'type' => 'integer' },
+ 'namespace_id' => { 'type' => 'integer' },
+ 'root_namespace_id' => { 'type' => 'integer' },
+ 'domain' => { 'type' => 'string' }
+ },
+ 'required' => %w[project_id namespace_id root_namespace_id]
+ }
+ end
+ end
+end
diff --git a/app/events/pages_domains/pages_domain_updated_event.rb b/app/events/pages_domains/pages_domain_updated_event.rb
new file mode 100644
index 00000000000..641fb2f6a53
--- /dev/null
+++ b/app/events/pages_domains/pages_domain_updated_event.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module PagesDomains
+ class PagesDomainUpdatedEvent < ::Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'project_id' => { 'type' => 'integer' },
+ 'namespace_id' => { 'type' => 'integer' },
+ 'root_namespace_id' => { 'type' => 'integer' },
+ 'domain' => { 'type' => 'string' }
+ },
+ 'required' => %w[project_id namespace_id root_namespace_id]
+ }
+ end
+ end
+end
diff --git a/app/events/projects/project_attributes_changed_event.rb b/app/events/projects/project_attributes_changed_event.rb
new file mode 100644
index 00000000000..f7c27fa65e6
--- /dev/null
+++ b/app/events/projects/project_attributes_changed_event.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Projects
+ class ProjectAttributesChangedEvent < ::Gitlab::EventStore::Event
+ PAGES_RELATED_ATTRIBUTES = %w[
+ pages_https_only
+ visibility_level
+ ].freeze
+
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'project_id' => { 'type' => 'integer' },
+ 'namespace_id' => { 'type' => 'integer' },
+ 'root_namespace_id' => { 'type' => 'integer' },
+ 'attributes' => { 'type' => 'array' }
+ },
+ 'required' => %w[project_id namespace_id root_namespace_id attributes]
+ }
+ end
+
+ def pages_related?
+ PAGES_RELATED_ATTRIBUTES.any? do |attribute|
+ data[:attributes].include?(attribute)
+ end
+ end
+ end
+end
diff --git a/app/events/projects/project_features_changed_event.rb b/app/events/projects/project_features_changed_event.rb
new file mode 100644
index 00000000000..a0c6fa1a3f9
--- /dev/null
+++ b/app/events/projects/project_features_changed_event.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Projects
+ class ProjectFeaturesChangedEvent < ::Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'properties' => {
+ 'project_id' => { 'type' => 'integer' },
+ 'namespace_id' => { 'type' => 'integer' },
+ 'root_namespace_id' => { 'type' => 'integer' },
+ 'features' => { 'type' => 'array' }
+ },
+ 'required' => %w[project_id namespace_id root_namespace_id features]
+ }
+ end
+
+ def pages_related?
+ data[:features].include?("pages_access_level")
+ end
+ end
+end
diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb
index 774947a35b7..d0d98a59677 100644
--- a/app/finders/ci/runners_finder.rb
+++ b/app/finders/ci/runners_finder.rb
@@ -55,6 +55,12 @@ module Ci
Ci::Runner.belonging_to_group(@group.id)
when :descendants, nil
Ci::Runner.belonging_to_group_or_project_descendants(@group.id)
+ when :all_available
+ unless can?(@current_user, :read_group_all_available_runners, @group)
+ raise Gitlab::Access::AccessDeniedError
+ end
+
+ Ci::Runner.usable_from_scope(@group)
else
raise ArgumentError, 'Invalid membership filter'
end
diff --git a/app/finders/clusters/agent_authorizations_finder.rb b/app/finders/clusters/agent_authorizations_finder.rb
index 373cf7fe8b9..8b939f5d646 100644
--- a/app/finders/clusters/agent_authorizations_finder.rb
+++ b/app/finders/clusters/agent_authorizations_finder.rb
@@ -24,13 +24,21 @@ module Clusters
# rubocop: disable CodeReuse/ActiveRecord
def project_authorizations
- ancestor_ids = project.group ? project.ancestors.select(:id) : project.namespace_id
+ namespace_ids = if project.group
+ if include_descendants?
+ all_namespace_ids
+ else
+ ancestor_namespace_ids
+ end
+ else
+ project.namespace_id
+ end
Clusters::Agents::ProjectAuthorization
.where(project_id: project.id)
.joins(agent: :project)
.preload(agent: :project)
- .where(cluster_agents: { projects: { namespace_id: ancestor_ids } })
+ .where(cluster_agents: { projects: { namespace_id: namespace_ids } })
.with_available_ci_access_fields(project)
.to_a
end
@@ -49,17 +57,35 @@ module Clusters
authorizations[:group_id].eq(ordered_ancestors_cte.table[:id])
).join_sources
- Clusters::Agents::GroupAuthorization
+ authorized_groups = Clusters::Agents::GroupAuthorization
.with(ordered_ancestors_cte.to_arel)
.joins(cte_join_sources)
.joins(agent: :project)
- .where('projects.namespace_id IN (SELECT id FROM ordered_ancestors)')
.with_available_ci_access_fields(project)
.order(Arel.sql('agent_id, array_position(ARRAY(SELECT id FROM ordered_ancestors)::bigint[], agent_group_authorizations.group_id)'))
.select('DISTINCT ON (agent_id) agent_group_authorizations.*')
.preload(agent: :project)
- .to_a
+
+ authorized_groups = if include_descendants?
+ authorized_groups.where(projects: { namespace_id: all_namespace_ids })
+ else
+ authorized_groups.where('projects.namespace_id IN (SELECT id FROM ordered_ancestors)')
+ end
+
+ authorized_groups.to_a
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def ancestor_namespace_ids
+ project.ancestors.select(:id)
+ end
+
+ def all_namespace_ids
+ project.root_ancestor.self_and_descendants.select(:id)
+ end
+
+ def include_descendants?
+ Feature.enabled?(:agent_authorization_include_descendants, project)
+ end
end
end
diff --git a/app/finders/groups/accepting_group_transfers_finder.rb b/app/finders/groups/accepting_group_transfers_finder.rb
index df67f940d20..c95318d0098 100644
--- a/app/finders/groups/accepting_group_transfers_finder.rb
+++ b/app/finders/groups/accepting_group_transfers_finder.rb
@@ -13,12 +13,7 @@ module Groups
def execute
return Group.none unless can_transfer_group?
- items = if Feature.enabled?(:include_groups_from_group_shares_in_group_transfer_locations)
- find_all_groups
- else
- find_groups
- end
-
+ items = find_all_groups
items = by_search(items)
sort(items)
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index ecd6270ed47..9f9d0da6efd 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -19,7 +19,7 @@ class LabelsFinder < UnionFinder
items = with_title(items)
items = by_subscription(items)
items = by_search(items)
- sort(items)
+ sort(items.with_preloaded_container)
end
private
diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb
index 1d1ae59674a..3a068252d5c 100644
--- a/app/finders/packages/group_packages_finder.rb
+++ b/app/finders/packages/group_packages_finder.rb
@@ -22,7 +22,7 @@ module Packages
def packages_for_group_projects(installable_only: false)
packages = ::Packages::Package
- .including_project_route
+ .including_project_namespace_route
.including_tags
.for_projects(group_projects_visible_to_current_user.select(:id))
.sort_by_attribute("#{params[:order_by]}_#{params[:sort]}")
diff --git a/app/finders/packages/helm/packages_finder.rb b/app/finders/packages/helm/packages_finder.rb
index c58d9292e9f..e1b831ca864 100644
--- a/app/finders/packages/helm/packages_finder.rb
+++ b/app/finders/packages/helm/packages_finder.rb
@@ -5,7 +5,7 @@ module Packages
class PackagesFinder
include ::Packages::FinderHelper
- MAX_PACKAGES_COUNT = 300
+ MAX_PACKAGES_COUNT = 1000
def initialize(project, channel)
@project = project
diff --git a/app/finders/packages/nuget/package_finder.rb b/app/finders/packages/nuget/package_finder.rb
index 9ae52745bb2..23345f29198 100644
--- a/app/finders/packages/nuget/package_finder.rb
+++ b/app/finders/packages/nuget/package_finder.rb
@@ -15,7 +15,7 @@ module Packages
result = base.nuget
.has_version
.with_name_like(@params[:package_name])
- result = result.with_version(@params[:package_version]) if @params[:package_version].present?
+ result = result.with_case_insensitive_version(@params[:package_version]) if @params[:package_version].present?
result
end
end
diff --git a/app/finders/packages/package_finder.rb b/app/finders/packages/package_finder.rb
index e482a0503f0..9e667b7a63c 100644
--- a/app/finders/packages/package_finder.rb
+++ b/app/finders/packages/package_finder.rb
@@ -10,7 +10,7 @@ module Packages
@project
.packages
.preload_pipelines
- .including_project_route
+ .including_project_namespace_route
.including_tags
.displayable
.find(@package_id)
diff --git a/app/finders/packages/packages_finder.rb b/app/finders/packages/packages_finder.rb
index b3d14e15953..31fbbfb7937 100644
--- a/app/finders/packages/packages_finder.rb
+++ b/app/finders/packages/packages_finder.rb
@@ -14,7 +14,7 @@ module Packages
def execute
packages = project.packages
- .including_project_route
+ .including_project_namespace_route
.including_tags
packages = packages.preload_pipelines if preload_pipelines
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index 7d356c1014c..8403c531945 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -18,6 +18,12 @@ class PersonalAccessTokensFinder
tokens = by_impersonation(tokens)
tokens = by_state(tokens)
tokens = by_owner_type(tokens)
+ tokens = by_revoked_state(tokens)
+ tokens = by_created_before(tokens)
+ tokens = by_created_after(tokens)
+ tokens = by_last_used_before(tokens)
+ tokens = by_last_used_after(tokens)
+ tokens = by_search(tokens)
sort(tokens)
end
@@ -83,4 +89,40 @@ class PersonalAccessTokensFinder
tokens
end
end
+
+ def by_revoked_state(tokens)
+ return tokens unless params.has_key?(:revoked)
+
+ params[:revoked] ? tokens.revoked : tokens.not_revoked
+ end
+
+ def by_created_before(tokens)
+ return tokens unless params[:created_before]
+
+ tokens.created_before(params[:created_before])
+ end
+
+ def by_created_after(tokens)
+ return tokens unless params[:created_after]
+
+ tokens.created_after(params[:created_after])
+ end
+
+ def by_last_used_before(tokens)
+ return tokens unless params[:last_used_before]
+
+ tokens.last_used_before(params[:last_used_before])
+ end
+
+ def by_last_used_after(tokens)
+ return tokens unless params[:last_used_after]
+
+ tokens.last_used_after(params[:last_used_after])
+ end
+
+ def by_search(tokens)
+ return tokens unless params[:search]
+
+ tokens.search(params[:search])
+ end
end
diff --git a/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb b/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb
new file mode 100644
index 00000000000..c1b35d3eaf7
--- /dev/null
+++ b/app/graphql/batch_loaders/award_emoji_votes_batch_loader.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module BatchLoaders
+ module AwardEmojiVotesBatchLoader
+ private
+
+ def load_votes(object, vote_type)
+ BatchLoader::GraphQL.for(object.id).batch(key: "#{object.issuing_parent_id}-#{vote_type}") do |ids, loader, args|
+ counts = AwardEmoji.votes_for_collection(ids, object.class.name).named(vote_type).index_by(&:awardable_id)
+
+ ids.each do |id|
+ loader.call(id, counts[id]&.count || 0)
+ end
+ end
+ end
+
+ def authorized_resource?(object)
+ Ability.allowed?(current_user, "read_#{object.to_ability_name}".to_sym, object)
+ end
+ end
+end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index c0e063a34d5..37adf4c2d3b 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -137,6 +137,19 @@ class GitlabSchema < GraphQL::Schema
gid
end
+ # Parse an array of strings to an array of GlobalIDs, raising ArgumentError if there are problems
+ # with it.
+ # See #parse_gid
+ #
+ # ```
+ # gids = GitlabSchema.parse_gids(my_array_of_strings, expected_type: ::Project)
+ # project_ids = gids.map(&:model_id)
+ # gids.all? { |gid| gid.model_class == ::Project }
+ # ```
+ def parse_gids(global_ids, ctx = {})
+ global_ids.map { |gid| parse_gid(gid, ctx) }
+ end
+
private
def max_query_complexity(ctx)
diff --git a/app/graphql/graphql_triggers.rb b/app/graphql/graphql_triggers.rb
index 8086d8c02a4..dc4f838ae36 100644
--- a/app/graphql/graphql_triggers.rb
+++ b/app/graphql/graphql_triggers.rb
@@ -13,6 +13,10 @@ module GraphqlTriggers
GitlabSchema.subscriptions.trigger('issuableTitleUpdated', { issuable_id: issuable.to_gid }, issuable)
end
+ def self.issuable_description_updated(issuable)
+ GitlabSchema.subscriptions.trigger('issuableDescriptionUpdated', { issuable_id: issuable.to_gid }, issuable)
+ end
+
def self.issuable_labels_updated(issuable)
GitlabSchema.subscriptions.trigger('issuableLabelsUpdated', { issuable_id: issuable.to_gid }, issuable)
end
@@ -20,6 +24,22 @@ module GraphqlTriggers
def self.issuable_dates_updated(issuable)
GitlabSchema.subscriptions.trigger('issuableDatesUpdated', { issuable_id: issuable.to_gid }, issuable)
end
+
+ def self.merge_request_reviewers_updated(merge_request)
+ GitlabSchema.subscriptions.trigger(
+ 'mergeRequestReviewersUpdated',
+ { issuable_id: merge_request.to_gid },
+ merge_request
+ )
+ end
+
+ def self.merge_request_merge_status_updated(merge_request)
+ GitlabSchema.subscriptions.trigger(
+ 'mergeRequestMergeStatusUpdated',
+ { issuable_id: merge_request.to_gid },
+ merge_request
+ )
+ end
end
GraphqlTriggers.prepend_mod
diff --git a/app/graphql/mutations/alert_management/create_alert_issue.rb b/app/graphql/mutations/alert_management/create_alert_issue.rb
index 2c128e1b339..77a7d7a4147 100644
--- a/app/graphql/mutations/alert_management/create_alert_issue.rb
+++ b/app/graphql/mutations/alert_management/create_alert_issue.rb
@@ -24,8 +24,8 @@ module Mutations
def prepare_response(alert, result)
{
alert: alert,
- issue: result.payload[:issue],
- errors: Array(result.message)
+ issue: result[:issue],
+ errors: result.errors
}
end
end
diff --git a/app/graphql/mutations/ci/job/artifacts_destroy.rb b/app/graphql/mutations/ci/job/artifacts_destroy.rb
index c27ab9c4d89..34c58fc1240 100644
--- a/app/graphql/mutations/ci/job/artifacts_destroy.rb
+++ b/app/graphql/mutations/ci/job/artifacts_destroy.rb
@@ -25,12 +25,21 @@ module Mutations
def resolve(id:)
job = authorized_find!(id: id)
- result = ::Ci::JobArtifacts::DestroyBatchService.new(job.job_artifacts, pick_up_at: Time.current).execute
- {
- job: job,
- destroyed_artifacts_count: result[:destroyed_artifacts_count],
- errors: Array(result[:errors])
- }
+ result = ::Ci::JobArtifacts::DeleteService.new(job).execute
+
+ if result.success?
+ {
+ job: job,
+ destroyed_artifacts_count: result.payload[:destroyed_artifacts_count],
+ errors: Array(result.payload[:errors])
+ }
+ else
+ {
+ job: job,
+ destroyed_artifacts_count: 0,
+ errors: Array(result.message)
+ }
+ end
end
end
end
diff --git a/app/graphql/mutations/ci/pipeline_schedule/base.rb b/app/graphql/mutations/ci/pipeline_schedule/base.rb
new file mode 100644
index 00000000000..a737ccce575
--- /dev/null
+++ b/app/graphql/mutations/ci/pipeline_schedule/base.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module PipelineSchedule
+ class Base < BaseMutation
+ PipelineScheduleID = ::Types::GlobalIDType[::Ci::PipelineSchedule]
+
+ argument :id, PipelineScheduleID,
+ required: true,
+ description: 'ID of the pipeline schedule to mutate.'
+
+ private
+
+ def find_object(id:)
+ GlobalID::Locator.locate(id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/pipeline_schedule/delete.rb b/app/graphql/mutations/ci/pipeline_schedule/delete.rb
new file mode 100644
index 00000000000..ead9a43161d
--- /dev/null
+++ b/app/graphql/mutations/ci/pipeline_schedule/delete.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module PipelineSchedule
+ class Delete < Base
+ graphql_name 'PipelineScheduleDelete'
+
+ authorize :admin_pipeline_schedule
+
+ def resolve(id:)
+ schedule = authorized_find!(id: id)
+
+ if schedule.destroy
+ {
+ errors: []
+ }
+ else
+ {
+ errors: ['Failed to remove the pipeline schedule']
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
index b0cffa2c088..27b066ffcf6 100644
--- a/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
+++ b/app/graphql/mutations/ci/project_ci_cd_settings_update.rb
@@ -19,7 +19,13 @@ module Mutations
argument :job_token_scope_enabled, GraphQL::Types::Boolean,
required: false,
- description: 'Indicates CI job tokens generated in this project have restricted access to resources.'
+ description: 'Indicates CI/CD job tokens generated in this project ' \
+ 'have restricted access to other projects.'
+
+ argument :inbound_job_token_scope_enabled, GraphQL::Types::Boolean,
+ required: false,
+ description: 'Indicates CI/CD job tokens generated in other projects ' \
+ 'have restricted access to this project.'
field :ci_cd_settings,
Types::Ci::CiCdSettingType,
@@ -28,6 +34,9 @@ module Mutations
def resolve(full_path:, **args)
project = authorized_find!(full_path)
+
+ args.delete(:inbound_job_token_scope_enabled) unless Feature.enabled?(:ci_inbound_job_token_scope, project)
+
settings = project.ci_cd_settings
settings.update(args)
diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb
index f98138646be..2f2c8c4c668 100644
--- a/app/graphql/mutations/ci/runner/update.rb
+++ b/app/graphql/mutations/ci/runner/update.rb
@@ -94,6 +94,7 @@ module Mutations
).execute
return if result.success?
+ response[:runner] = nil
response[:errors] = result.errors
raise ActiveRecord::Rollback
end
@@ -102,6 +103,7 @@ module Mutations
result = ::Ci::Runners::UpdateRunnerService.new(runner).execute(attrs)
return if result.success?
+ response[:runner] = nil
response[:errors] = result.errors
raise ActiveRecord::Rollback
end
diff --git a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
index 1f90f394521..e42e59de78f 100644
--- a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
+++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
@@ -30,6 +30,9 @@ module Mutations
argument :start_and_due_date_widget, ::Types::WorkItems::Widgets::StartAndDueDateUpdateInputType,
required: false,
description: 'Input for start and due date widget.'
+ argument :labels_widget, ::Types::WorkItems::Widgets::LabelsUpdateInputType,
+ required: false,
+ description: 'Input for labels widget.'
end
end
end
diff --git a/app/graphql/mutations/issues/create.rb b/app/graphql/mutations/issues/create.rb
index 6bf8caf82d7..0389a482822 100644
--- a/app/graphql/mutations/issues/create.rb
+++ b/app/graphql/mutations/issues/create.rb
@@ -83,13 +83,13 @@ module Mutations
params = build_create_issue_params(attributes.merge(author_id: current_user.id), project)
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
- issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
+ result = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
- check_spam_action_response!(issue)
+ check_spam_action_response!(result[:issue]) if result[:issue]
{
- issue: issue.valid? ? issue : nil,
- errors: errors_on_object(issue)
+ issue: result.success? ? result[:issue] : nil,
+ errors: result.errors
}
end
diff --git a/app/graphql/mutations/namespace/package_settings/update.rb b/app/graphql/mutations/namespace/package_settings/update.rb
index e499e646781..ea72b71715c 100644
--- a/app/graphql/mutations/namespace/package_settings/update.rb
+++ b/app/graphql/mutations/namespace/package_settings/update.rb
@@ -35,6 +35,36 @@ module Mutations
required: false,
description: copy_field_description(Types::Namespace::PackageSettingsType, :generic_duplicate_exception_regex)
+ argument :maven_package_requests_forwarding,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: copy_field_description(Types::Namespace::PackageSettingsType, :maven_package_requests_forwarding)
+
+ argument :npm_package_requests_forwarding,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: copy_field_description(Types::Namespace::PackageSettingsType, :npm_package_requests_forwarding)
+
+ argument :pypi_package_requests_forwarding,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: copy_field_description(Types::Namespace::PackageSettingsType, :pypi_package_requests_forwarding)
+
+ argument :lock_maven_package_requests_forwarding,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: copy_field_description(Types::Namespace::PackageSettingsType, :lock_maven_package_requests_forwarding)
+
+ argument :lock_npm_package_requests_forwarding,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: copy_field_description(Types::Namespace::PackageSettingsType, :lock_npm_package_requests_forwarding)
+
+ argument :lock_pypi_package_requests_forwarding,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: copy_field_description(Types::Namespace::PackageSettingsType, :lock_pypi_package_requests_forwarding)
+
field :package_settings,
Types::Namespace::PackageSettingsType,
null: true,
diff --git a/app/graphql/mutations/packages/bulk_destroy.rb b/app/graphql/mutations/packages/bulk_destroy.rb
new file mode 100644
index 00000000000..a0756d0c3f9
--- /dev/null
+++ b/app/graphql/mutations/packages/bulk_destroy.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Packages
+ class BulkDestroy < ::Mutations::BaseMutation
+ graphql_name 'DestroyPackages'
+
+ MAX_PACKAGES = 20
+ TOO_MANY_IDS_ERROR = "Cannot delete more than #{MAX_PACKAGES} packages"
+
+ argument :ids,
+ [::Types::GlobalIDType[::Packages::Package]],
+ required: true,
+ description: "Global IDs of the Packages. Max #{MAX_PACKAGES}"
+
+ def resolve(ids:)
+ raise_resource_not_available_error!(TOO_MANY_IDS_ERROR) if ids.size > MAX_PACKAGES
+
+ ids = GitlabSchema.parse_gids(ids, expected_type: ::Packages::Package)
+ .map(&:model_id)
+
+ service = ::Packages::MarkPackagesForDestructionService.new(
+ packages: packages_from(ids),
+ current_user: current_user
+ )
+ result = service.execute
+
+ raise_resource_not_available_error! if result.reason == :unauthorized
+
+ errors = result.error? ? Array.wrap(result[:message]) : []
+
+ { errors: errors }
+ end
+
+ private
+
+ def packages_from(ids)
+ ::Packages::Package.displayable
+ .id_in(ids)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/packages/destroy_files.rb b/app/graphql/mutations/packages/destroy_files.rb
index 3900a2c46ae..60a21be20d8 100644
--- a/app/graphql/mutations/packages/destroy_files.rb
+++ b/app/graphql/mutations/packages/destroy_files.rb
@@ -25,7 +25,7 @@ module Mutations
project = authorized_find!(project_path)
raise_resource_not_available_error! "Cannot delete more than #{MAXIMUM_FILES} files" if ids.size > MAXIMUM_FILES
- package_files = ::Packages::PackageFile.where(id: parse_gids(ids)) # rubocop:disable CodeReuse/ActiveRecord
+ package_files = ::Packages::PackageFile.id_in(parse_gids(ids))
ensure_file_access!(project, package_files)
@@ -47,7 +47,7 @@ module Mutations
end
def parse_gids(gids)
- gids.map { |gid| GitlabSchema.parse_gid(gid, expected_type: ::Packages::PackageFile).model_id }
+ GitlabSchema.parse_gids(gids, expected_type: ::Packages::PackageFile).map(&:model_id)
end
end
end
diff --git a/app/graphql/mutations/work_items/update_widgets.rb b/app/graphql/mutations/work_items/update_widgets.rb
deleted file mode 100644
index 7037b7e5a2a..00000000000
--- a/app/graphql/mutations/work_items/update_widgets.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-# frozen_string_literal: true
-
-module Mutations
- module WorkItems
- # TODO: Deprecate in favor of using WorkItemUpdate. See https://gitlab.com/gitlab-org/gitlab/-/issues/366300
- class UpdateWidgets < BaseMutation
- graphql_name 'WorkItemUpdateWidgets'
- description "Updates the attributes of a work item's widgets by global ID." \
- " Available only when feature flag `work_items` is enabled."
-
- include Mutations::SpamProtection
-
- authorize :update_work_item
-
- argument :id, ::Types::GlobalIDType[::WorkItem],
- required: true,
- description: 'Global ID of the work item.'
-
- argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType,
- required: false,
- description: 'Input for description widget.'
-
- field :work_item, Types::WorkItemType,
- null: true,
- description: 'Updated work item.'
-
- def resolve(id:, **widget_attributes)
- work_item = authorized_find!(id: id)
-
- unless work_item.project.work_items_feature_flag_enabled?
- return { errors: ['`work_items` feature flag disabled for this project'] }
- end
-
- spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
-
- ::WorkItems::UpdateService.new(
- project: work_item.project,
- current_user: current_user,
- # Cannot use prepare to use `.to_h` on each input due to
- # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87472#note_945199865
- widget_params: widget_attributes.transform_values { |values| values.to_h },
- spam_params: spam_params
- ).execute(work_item)
-
- check_spam_action_response!(work_item)
-
- {
- work_item: work_item.valid? ? work_item : nil,
- errors: errors_on_object(work_item)
- }
- end
-
- private
-
- def find_object(id:)
- GitlabSchema.find_by_gid(id)
- end
- end
- end
-end
diff --git a/app/graphql/resolvers/base_issues_resolver.rb b/app/graphql/resolvers/base_issues_resolver.rb
index ec47a8996eb..6357132705e 100644
--- a/app/graphql/resolvers/base_issues_resolver.rb
+++ b/app/graphql/resolvers/base_issues_resolver.rb
@@ -47,8 +47,8 @@ module Resolvers
def preloads
{
alert_management_alert: [:alert_management_alert],
- labels: [:labels],
assignees: [:assignees],
+ participants: Issue.participant_includes,
timelogs: [:timelogs],
customer_relations_contacts: { customer_relations_contacts: [:group] },
escalation_status: [:incident_management_issuable_escalation_status]
diff --git a/app/graphql/resolvers/bulk_labels_resolver.rb b/app/graphql/resolvers/bulk_labels_resolver.rb
new file mode 100644
index 00000000000..7362e257fb6
--- /dev/null
+++ b/app/graphql/resolvers/bulk_labels_resolver.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class BulkLabelsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::LabelType.connection_type, null: true
+
+ def resolve
+ authorize!(object)
+
+ BatchLoader::GraphQL.for(object.id).batch(cache: false) do |ids, loader, args|
+ labels = Label.for_targets(object.class.id_in(ids)).group_by(&:target_id)
+
+ ids.each do |id|
+ loader.call(id, labels[id] || [])
+ end
+ end
+ end
+
+ private
+
+ def authorized_resource?(object)
+ Ability.allowed?(current_user, :read_label, object.issuing_parent)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/all_jobs_resolver.rb b/app/graphql/resolvers/ci/all_jobs_resolver.rb
new file mode 100644
index 00000000000..d918bed9f57
--- /dev/null
+++ b/app/graphql/resolvers/ci/all_jobs_resolver.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class AllJobsResolver < BaseResolver
+ type ::Types::Ci::JobType.connection_type, null: true
+
+ argument :statuses, [::Types::Ci::JobStatusEnum],
+ required: false,
+ description: 'Filter jobs by status.'
+
+ def resolve(statuses: nil)
+ ::Ci::JobsFinder.new(current_user: current_user, params: { scope: statuses }).execute
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/runner_projects_resolver.rb b/app/graphql/resolvers/ci/runner_projects_resolver.rb
index ca3b4ebb797..af9a67acfda 100644
--- a/app/graphql/resolvers/ci/runner_projects_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_projects_resolver.rb
@@ -21,8 +21,8 @@ module Resolvers
'Specify `"id_asc"` if query results\' order is important',
milestone: '15.4'
},
- description: "Sort order of results. Format: '<field_name>_<sort_direction>', " \
- "for example: 'id_desc' or 'name_asc'"
+ description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \
+ "for example: `id_desc` or `name_asc`"
def resolve_with_lookahead(**args)
return unless runner.project_type?
diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb
index b548dc1e175..81099c04e9f 100644
--- a/app/graphql/resolvers/concerns/looks_ahead.rb
+++ b/app/graphql/resolvers/concerns/looks_ahead.rb
@@ -32,16 +32,37 @@ module LooksAhead
{}
end
+ def nested_preloads
+ {}
+ end
+
def filtered_preloads
nodes = node_selection
return [] unless nodes
selected_fields = nodes.selections.map(&:name)
+ root_level_preloads = preloads_from_node_selection(selected_fields, preloads)
- preloads.each.flat_map do |name, requirements|
- selected_fields.include?(name) ? requirements : []
- end
+ root_level_preloads + nested_filtered_preloads(nodes, selected_fields)
+ end
+
+ def nested_filtered_preloads(nodes, selected_root_fields)
+ return [] if nested_preloads.empty?
+
+ nested_preloads.each_with_object([]) do |(root_field, fields), result|
+ next unless selected_root_fields.include?(root_field)
+
+ selected_fields = nodes.selection(root_field).selections.map(&:name)
+
+ result << preloads_from_node_selection(selected_fields, fields)
+ end.flatten
+ end
+
+ def preloads_from_node_selection(selected_fields, fields)
+ fields.each_with_object([]) do |(field, requirements), result|
+ result << requirements if selected_fields.include?(field)
+ end.flatten
end
def node_selection
diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
index 697cc6f5b03..d56951bc821 100644
--- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb
+++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
@@ -42,7 +42,6 @@ module ResolvesMergeRequests
assignees: [:assignees],
reviewers: [:reviewers],
participants: MergeRequest.participant_includes,
- labels: [:labels],
author: [:author],
merged_at: [:metrics],
commit_count: [:metrics],
@@ -53,7 +52,8 @@ module ResolvesMergeRequests
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }],
timelogs: [:timelogs],
pipelines: [:merge_request_diffs], # used by `recent_diff_head_shas` to load pipelines
- committers: [merge_request_diff: [:merge_request_diff_commits]]
+ committers: [merge_request_diff: [:merge_request_diff_commits]],
+ suggested_reviewers: [:predictions]
}
end
end
diff --git a/app/graphql/resolvers/down_votes_count_resolver.rb b/app/graphql/resolvers/down_votes_count_resolver.rb
new file mode 100644
index 00000000000..0e7772f988a
--- /dev/null
+++ b/app/graphql/resolvers/down_votes_count_resolver.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class DownVotesCountResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include BatchLoaders::AwardEmojiVotesBatchLoader
+
+ type GraphQL::Types::Int, null: true
+
+ def resolve
+ authorize!(object)
+ load_votes(object, AwardEmoji::DOWNVOTE_NAME)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_pipeline_schedules_resolver.rb b/app/graphql/resolvers/project_pipeline_schedules_resolver.rb
new file mode 100644
index 00000000000..eb980f72717
--- /dev/null
+++ b/app/graphql/resolvers/project_pipeline_schedules_resolver.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class ProjectPipelineSchedulesResolver < BaseResolver
+ alias_method :project, :object
+
+ type ::Types::Ci::PipelineScheduleType.connection_type, null: true
+
+ argument :status, ::Types::Ci::PipelineScheduleStatusEnum,
+ required: false,
+ description: 'Filter pipeline schedules by active status.'
+
+ def resolve(status: nil)
+ ::Ci::PipelineSchedulesFinder.new(project).execute(scope: status)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/projects/branch_rules_resolver.rb b/app/graphql/resolvers/projects/branch_rules_resolver.rb
index 6c8b416bcea..e99d7ae4d5f 100644
--- a/app/graphql/resolvers/projects/branch_rules_resolver.rb
+++ b/app/graphql/resolvers/projects/branch_rules_resolver.rb
@@ -3,13 +3,17 @@
module Resolvers
module Projects
class BranchRulesResolver < BaseResolver
+ include LooksAhead
+
type Types::Projects::BranchRuleType.connection_type, null: false
alias_method :project, :object
- def resolve(**args)
- project.protected_branches
+ def resolve_with_lookahead(**args)
+ apply_lookahead(project.protected_branches)
end
end
end
end
+
+Resolvers::Projects::BranchRulesResolver.prepend_mod_with('Resolvers::Projects::BranchRulesResolver')
diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb
index 4d1e1b867da..0bdba53c7af 100644
--- a/app/graphql/resolvers/projects_resolver.rb
+++ b/app/graphql/resolvers/projects_resolver.rb
@@ -12,8 +12,8 @@ module Resolvers
argument :sort, GraphQL::Types::String,
required: false,
- description: "Sort order of results. Format: '<field_name>_<sort_direction>', " \
- "for example: 'id_desc' or 'name_asc'"
+ description: "Sort order of results. Format: `<field_name>_<sort_direction>`, " \
+ "for example: `id_desc` or `name_asc`"
def resolve(**args)
ProjectsFinder
diff --git a/app/graphql/resolvers/up_votes_count_resolver.rb b/app/graphql/resolvers/up_votes_count_resolver.rb
new file mode 100644
index 00000000000..1c78facb694
--- /dev/null
+++ b/app/graphql/resolvers/up_votes_count_resolver.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class UpVotesCountResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include BatchLoaders::AwardEmojiVotesBatchLoader
+
+ type GraphQL::Types::Int, null: true
+
+ def resolve
+ authorize!(object)
+ load_votes(object, AwardEmoji::UPVOTE_NAME)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb
index a8c0d363325..a4cbcc61ead 100644
--- a/app/graphql/resolvers/work_items_resolver.rb
+++ b/app/graphql/resolvers/work_items_resolver.rb
@@ -37,20 +37,26 @@ module Resolvers
def preloads
{
- last_edited_by: :last_edited_by,
- web_url: { project: { namespace: :route } }
+ work_item_type: :work_item_type,
+ web_url: { project: { namespace: :route } },
+ widgets: :work_item_type
}
end
- # Allows to apply lookahead for fields
- # selected from WidgetInterface
- override :node_selection
- def node_selection
- selected_fields = super
-
- return unless selected_fields
+ def nested_preloads
+ {
+ widgets: widget_preloads,
+ user_permissions: { update_work_item: :assignees }
+ }
+ end
- selected_fields.selection(:widgets)
+ def widget_preloads
+ {
+ last_edited_by: :last_edited_by,
+ assignees: :assignees,
+ parent: :work_item_parent,
+ labels: :labels
+ }
end
def unconditional_includes
diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb
index bec8c72e783..574791b79e6 100644
--- a/app/graphql/types/ci/ci_cd_setting_type.rb
+++ b/app/graphql/types/ci/ci_cd_setting_type.rb
@@ -10,8 +10,17 @@ module Types
field :job_token_scope_enabled,
GraphQL::Types::Boolean,
null: true,
- description: 'Indicates CI job tokens generated in this project have restricted access to resources.',
+ description: 'Indicates CI/CD job tokens generated in this project ' \
+ 'have restricted access to other projects.',
method: :job_token_scope_enabled?
+
+ field :inbound_job_token_scope_enabled,
+ GraphQL::Types::Boolean,
+ null: true,
+ description: 'Indicates CI/CD job tokens generated in other projects ' \
+ 'have restricted access to this project.',
+ method: :inbound_job_token_scope_enabled?
+
field :keep_latest_artifact, GraphQL::Types::Boolean, null: true,
description: 'Whether to keep the latest builds artifacts.',
method: :keep_latest_artifacts_available?
diff --git a/app/graphql/types/ci/config_variable_type.rb b/app/graphql/types/ci/config_variable_type.rb
index 87ae026c2c1..5b5890fd5a5 100644
--- a/app/graphql/types/ci/config_variable_type.rb
+++ b/app/graphql/types/ci/config_variable_type.rb
@@ -17,6 +17,10 @@ module Types
field :value, GraphQL::Types::String,
null: true,
description: 'Value of the variable.'
+
+ field :value_options, [GraphQL::Types::String],
+ null: true,
+ description: 'Value options for the variable.'
end
end
end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index ab6103d9469..4447a10a74e 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -56,6 +56,8 @@ module Types
description: 'Indicates the job is active.'
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
description: 'Artifacts generated by the job.'
+ field :browse_artifacts_path, GraphQL::Types::String, null: true,
+ description: "URL for browsing the artifact's archive."
field :cancelable, GraphQL::Types::Boolean, null: false, method: :cancelable?,
description: 'Indicates the job can be canceled.'
field :commit_path, GraphQL::Types::String, null: true,
@@ -148,17 +150,7 @@ module Types
end
def stage
- ::Gitlab::Graphql::Lazy.with_value(pipeline) do |pl|
- BatchLoader::GraphQL.for([pl, object.stage]).batch do |ids, loader|
- by_pipeline = ids
- .group_by(&:first)
- .transform_values { |grp| grp.map(&:second) }
-
- by_pipeline.each do |p, names|
- p.stages.by_name(names).each { |s| loader.call([p, s.name], s) }
- end
- end
- end
+ ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Stage, object.stage_id).find
end
# This class is a secret union!
@@ -187,6 +179,10 @@ module Types
::Gitlab::Routing.url_helpers.project_job_path(object.project, object)
end
+ def browse_artifacts_path
+ ::Gitlab::Routing.url_helpers.browse_project_job_artifacts_path(object.project, object)
+ end
+
def coverage
object&.coverage
end
diff --git a/app/graphql/types/ci/pipeline_schedule_status_enum.rb b/app/graphql/types/ci/pipeline_schedule_status_enum.rb
new file mode 100644
index 00000000000..61bae7daff8
--- /dev/null
+++ b/app/graphql/types/ci/pipeline_schedule_status_enum.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class PipelineScheduleStatusEnum < BaseEnum
+ graphql_name 'PipelineScheduleStatus'
+
+ value 'ACTIVE', value: "active", description: 'Active pipeline schedules.'
+ value 'INACTIVE', value: "inactive", description: 'Inactive pipeline schedules.'
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_schedule_type.rb b/app/graphql/types/ci/pipeline_schedule_type.rb
new file mode 100644
index 00000000000..04f9fc78a92
--- /dev/null
+++ b/app/graphql/types/ci/pipeline_schedule_type.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class PipelineScheduleType < BaseObject
+ graphql_name 'PipelineSchedule'
+
+ connection_type_class(Types::CountableConnectionType)
+
+ expose_permissions Types::PermissionTypes::Ci::PipelineSchedules
+
+ authorize :read_pipeline_schedule
+
+ field :id, GraphQL::Types::ID, null: false, description: 'ID of the pipeline schedule.'
+
+ field :description, GraphQL::Types::String, null: true, description: 'Description of the pipeline schedule.'
+
+ field :owner, ::Types::UserType, null: false, description: 'Owner of the pipeline schedule.'
+
+ field :active, GraphQL::Types::Boolean, null: false, description: 'Indicates if a pipeline schedule is active.'
+
+ field :next_run_at, Types::TimeType, null: false, description: 'Time when the next pipeline will run.'
+
+ field :real_next_run, Types::TimeType, null: false, description: 'Time when the next pipeline will run.'
+
+ field :last_pipeline, PipelineType, null: true, description: 'Last pipeline object.'
+
+ field :ref_for_display, GraphQL::Types::String,
+ null: true, description: 'Git ref for the pipeline schedule.', method: :ref_for_display
+
+ field :ref_path, GraphQL::Types::String, null: true, description: 'Path to the ref that triggered the pipeline.'
+
+ field :for_tag, GraphQL::Types::Boolean,
+ null: false, description: 'Indicates if a pipelines schedule belongs to a tag.', method: :for_tag?
+
+ field :cron, GraphQL::Types::String, null: false, description: 'Cron notation for the schedule.'
+
+ field :cron_timezone, GraphQL::Types::String, null: false, description: 'Timezone for the pipeline schedule.'
+
+ def ref_path
+ ::Gitlab::Routing.url_helpers.project_commits_path(object.project, object.ref_for_display)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_membership_filter_enum.rb b/app/graphql/types/ci/runner_membership_filter_enum.rb
index 4fd7e0749b0..d59a68b427b 100644
--- a/app/graphql/types/ci/runner_membership_filter_enum.rb
+++ b/app/graphql/types/ci/runner_membership_filter_enum.rb
@@ -15,6 +15,13 @@ module Types
description: "Include runners that have either a direct or inherited relationship. " \
"These runners can be specific to a project or a group.",
value: :descendants
+
+ value 'ALL_AVAILABLE',
+ description:
+ "Include all runners. This list includes runners for all projects in the group " \
+ "and subgroups, as well as for the parent groups and instance.",
+ value: :all_available,
+ deprecated: { milestone: '15.5', reason: :alpha }
end
end
end
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
index eb4e7b1dabf..dd2286d333d 100644
--- a/app/graphql/types/environment_type.rb
+++ b/app/graphql/types/environment_type.rb
@@ -57,7 +57,7 @@ module Types
field :deployments,
Types::DeploymentType.connection_type,
null: true,
- description: 'Deployments of the environment. This field can only be resolved for one project in any single request.',
+ description: 'Deployments of the environment. This field can only be resolved for one environment in any single request.',
resolver: Resolvers::DeploymentsResolver do
extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
end
@@ -72,3 +72,5 @@ module Types
end
end
end
+
+Types::EnvironmentType.prepend_mod_with('Types::EnvironmentType')
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index d897f3cde48..76fac831199 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -43,8 +43,10 @@ module Types
field :updated_by, Types::UserType, null: true,
description: 'User that last updated the issue.'
- field :labels, Types::LabelType.connection_type, null: true,
- description: 'Labels of the issue.'
+ field :labels, Types::LabelType.connection_type,
+ null: true,
+ description: 'Labels of the issue.',
+ resolver: Resolvers::BulkLabelsResolver
field :milestone, Types::MilestoneType, null: true,
description: 'Milestone of the issue.'
@@ -58,15 +60,20 @@ module Types
description: 'Indicates the issue is hidden because the author has been banned. ' \
'Will always return `null` if `ban_user_feature_flag` feature flag is disabled.'
- field :downvotes, GraphQL::Types::Int, null: false,
- description: 'Number of downvotes the issue has received.'
+ field :downvotes, GraphQL::Types::Int,
+ null: false,
+ description: 'Number of downvotes the issue has received.',
+ resolver: Resolvers::DownVotesCountResolver
field :merge_requests_count, GraphQL::Types::Int, null: false,
description: 'Number of merge requests that close the issue on merge.',
resolver: Resolvers::MergeRequestsCountResolver
field :relative_position, GraphQL::Types::Int, null: true,
description: 'Relative position of the issue (used for positioning in epic tree and issue boards).'
- field :upvotes, GraphQL::Types::Int, null: false,
- description: 'Number of upvotes the issue has received.'
+ field :upvotes, GraphQL::Types::Int,
+ null: false,
+ description: 'Number of upvotes the issue has received.',
+ resolver: Resolvers::UpVotesCountResolver
+
field :user_discussions_count, GraphQL::Types::Int, null: false,
description: 'Number of user discussions in the issue.',
resolver: Resolvers::UserDiscussionsCountResolver
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 399dcc8e03d..8cc600fc68e 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -75,8 +75,12 @@ module Types
null: false, calls_gitaly: true,
method: :diverged_from_target_branch?,
description: 'Indicates if the source branch is behind the target branch.'
- field :downvotes, GraphQL::Types::Int, null: false,
- description: 'Number of downvotes for the merge request.'
+
+ field :downvotes, GraphQL::Types::Int,
+ null: false,
+ description: 'Number of downvotes for the merge request.',
+ resolver: Resolvers::DownVotesCountResolver
+
field :force_remove_source_branch, GraphQL::Types::Boolean, method: :force_remove_source_branch?, null: true,
description: 'Indicates if the project settings will lead to source branch deletion after merge.'
field :in_progress_merge_commit_sha, GraphQL::Types::String, null: true,
@@ -118,8 +122,12 @@ module Types
null: false, calls_gitaly: true,
method: :target_branch_exists?,
description: 'Indicates if the target branch of the merge request exists.'
- field :upvotes, GraphQL::Types::Int, null: false,
- description: 'Number of upvotes for the merge request.'
+
+ field :upvotes, GraphQL::Types::Int,
+ null: false,
+ description: 'Number of upvotes for the merge request.',
+ resolver: Resolvers::UpVotesCountResolver
+
field :user_discussions_count, GraphQL::Types::Int, null: true,
description: 'Number of user discussions in the merge request.',
resolver: Resolvers::UserDiscussionsCountResolver
@@ -150,8 +158,11 @@ module Types
description: 'Human-readable time estimate of the merge request.'
field :human_total_time_spent, GraphQL::Types::String, null: true,
description: 'Human-readable total time reported as spent on the merge request.'
- field :labels, Types::LabelType.connection_type, null: true, complexity: 5,
- description: 'Labels of the merge request.'
+ field :labels, Types::LabelType.connection_type,
+ null: true, complexity: 5,
+ description: 'Labels of the merge request.',
+ resolver: Resolvers::BulkLabelsResolver
+
field :milestone, Types::MilestoneType, null: true,
description: 'Milestone of the merge request.'
field :participants, Types::MergeRequests::ParticipantType.connection_type, null: true, complexity: 15,
diff --git a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb
index 3de6296154d..1ba72ae33b5 100644
--- a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb
+++ b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb
@@ -42,6 +42,9 @@ module Types
value 'POLICIES_DENIED',
value: :policies_denied,
description: 'There are denied policies for the merge request.'
+ value 'EXTERNAL_STATUS_CHECKS',
+ value: :status_checks_must_pass,
+ description: 'Status checks must pass.'
end
end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index ea833b35085..5ffc1aeacad 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -114,6 +114,7 @@ module Types
mount_mutation Mutations::Ci::Pipeline::Cancel
mount_mutation Mutations::Ci::Pipeline::Destroy
mount_mutation Mutations::Ci::Pipeline::Retry
+ mount_mutation Mutations::Ci::PipelineSchedule::Delete
mount_mutation Mutations::Ci::CiCdSettingsUpdate, deprecated: {
reason: :renamed,
replacement: 'ProjectCiCdSettingsUpdate',
@@ -137,6 +138,8 @@ module Types
mount_mutation Mutations::UserCallouts::Create
mount_mutation Mutations::UserPreferences::Update
mount_mutation Mutations::Packages::Destroy
+ mount_mutation Mutations::Packages::BulkDestroy,
+ extensions: [::Gitlab::Graphql::Limit::FieldCallCount => { limit: 1 }]
mount_mutation Mutations::Packages::DestroyFile
mount_mutation Mutations::Packages::DestroyFiles
mount_mutation Mutations::Packages::Cleanup::Policy::Update
@@ -146,7 +149,6 @@ module Types
mount_mutation Mutations::WorkItems::Delete, alpha: { milestone: '15.1' }
mount_mutation Mutations::WorkItems::DeleteTask, alpha: { milestone: '15.1' }
mount_mutation Mutations::WorkItems::Update, alpha: { milestone: '15.1' }
- mount_mutation Mutations::WorkItems::UpdateWidgets, alpha: { milestone: '15.1' }
mount_mutation Mutations::WorkItems::UpdateTask, alpha: { milestone: '15.1' }
mount_mutation Mutations::SavedReplies::Create
mount_mutation Mutations::SavedReplies::Update
diff --git a/app/graphql/types/namespace/package_settings_type.rb b/app/graphql/types/namespace/package_settings_type.rb
index 7a0abe619a5..84becba8001 100644
--- a/app/graphql/types/namespace/package_settings_type.rb
+++ b/app/graphql/types/namespace/package_settings_type.rb
@@ -8,9 +8,50 @@ module Types
authorize :admin_package
- field :generic_duplicate_exception_regex, Types::UntrustedRegexp, null: true, description: 'When generic_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
- field :generic_duplicates_allowed, GraphQL::Types::Boolean, null: false, description: 'Indicates whether duplicate generic packages are allowed for this namespace.'
- field :maven_duplicate_exception_regex, Types::UntrustedRegexp, null: true, description: 'When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
- field :maven_duplicates_allowed, GraphQL::Types::Boolean, null: false, description: 'Indicates whether duplicate Maven packages are allowed for this namespace.'
+ field :generic_duplicate_exception_regex, Types::UntrustedRegexp,
+ null: true,
+ description: 'When generic_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
+ field :generic_duplicates_allowed, GraphQL::Types::Boolean,
+ null: false,
+ description: 'Indicates whether duplicate generic packages are allowed for this namespace.'
+ field :maven_duplicate_exception_regex, Types::UntrustedRegexp,
+ null: true,
+ description: 'When maven_duplicates_allowed is false, you can publish duplicate packages with names that match this regex. Otherwise, this setting has no effect.'
+ field :maven_duplicates_allowed, GraphQL::Types::Boolean,
+ null: false,
+ description: 'Indicates whether duplicate Maven packages are allowed for this namespace.'
+
+ field :maven_package_requests_forwarding, GraphQL::Types::Boolean,
+ null: true,
+ description: 'Indicates whether Maven package forwarding is allowed for this namespace.'
+ field :npm_package_requests_forwarding, GraphQL::Types::Boolean,
+ null: true,
+ description: 'Indicates whether npm package forwarding is allowed for this namespace.'
+ field :pypi_package_requests_forwarding, GraphQL::Types::Boolean,
+ null: true,
+ description: 'Indicates whether PyPI package forwarding is allowed for this namespace.'
+
+ field :lock_maven_package_requests_forwarding, GraphQL::Types::Boolean,
+ null: false,
+ description: 'Indicates whether Maven package forwarding is locked for all descendent namespaces.'
+ field :lock_npm_package_requests_forwarding, GraphQL::Types::Boolean,
+ null: false,
+ description: 'Indicates whether npm package forwarding is locked for all descendent namespaces.'
+ field :lock_pypi_package_requests_forwarding, GraphQL::Types::Boolean,
+ null: false,
+ description: 'Indicates whether PyPI package forwarding is locked for all descendent namespaces.'
+
+ field :maven_package_requests_forwarding_locked, GraphQL::Types::Boolean,
+ null: false,
+ method: :maven_package_requests_forwarding_locked?,
+ description: 'Indicates whether Maven package forwarding settings are locked by a parent namespace.'
+ field :npm_package_requests_forwarding_locked, GraphQL::Types::Boolean,
+ null: false,
+ method: :npm_package_requests_forwarding_locked?,
+ description: 'Indicates whether npm package forwarding settings are locked by a parent namespace.'
+ field :pypi_package_requests_forwarding_locked, GraphQL::Types::Boolean,
+ null: false,
+ method: :pypi_package_requests_forwarding_locked?,
+ description: 'Indicates whether PyPI package forwarding settings are locked by a parent namespace.'
end
end
diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb
index c254460a51f..eef5ce40bde 100644
--- a/app/graphql/types/notes/note_type.rb
+++ b/app/graphql/types/notes/note_type.rb
@@ -41,7 +41,7 @@ module Types
deprecated: {
reason: :renamed,
replacement: 'internal',
- milestone: '15.3'
+ milestone: '15.5'
}
field :internal, GraphQL::Types::Boolean, null: true,
diff --git a/app/graphql/types/permission_types/ci/pipeline_schedules.rb b/app/graphql/types/permission_types/ci/pipeline_schedules.rb
new file mode 100644
index 00000000000..268ac6096d0
--- /dev/null
+++ b/app/graphql/types/permission_types/ci/pipeline_schedules.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ module Ci
+ class PipelineSchedules < BasePermissionType
+ graphql_name 'PipelineSchedulePermissions'
+
+ abilities :take_ownership_pipeline_schedule,
+ :update_pipeline_schedule,
+ :admin_pipeline_schedule
+
+ ability_field :play_pipeline_schedule, calls_gitaly: true
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index f43f5c27dac..a41af34ef4c 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -309,6 +309,12 @@ module Types
extras: [:lookahead],
resolver: Resolvers::ProjectPipelinesResolver
+ field :pipeline_schedules,
+ type: Types::Ci::PipelineScheduleType.connection_type,
+ null: true,
+ description: 'Pipeline schedules of the project. This field can only be resolved for one project per request.',
+ resolver: Resolvers::ProjectPipelineSchedulesResolver
+
field :pipeline, Types::Ci::PipelineType,
null: true,
description: 'Build pipeline of the project.',
diff --git a/app/graphql/types/projects/branch_rule_type.rb b/app/graphql/types/projects/branch_rule_type.rb
index 866cff0f439..e7632c17cca 100644
--- a/app/graphql/types/projects/branch_rule_type.rb
+++ b/app/graphql/types/projects/branch_rule_type.rb
@@ -13,6 +13,13 @@ module Types
null: false,
description: 'Branch name, with wildcards, for the branch rules.'
+ field :is_default,
+ type: GraphQL::Types::Boolean,
+ null: false,
+ method: :default_branch?,
+ calls_gitaly: true,
+ description: "Check if this branch rule protects the project's default branch."
+
field :branch_protection,
type: Types::BranchRules::BranchProtectionType,
null: false,
@@ -31,3 +38,5 @@ module Types
end
end
end
+
+Types::Projects::BranchRuleType.prepend_mod
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 78463a1804a..1b39f43659e 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -154,6 +154,12 @@ module Types
null: true,
description: "Whether Gitpod is enabled in application settings."
+ field :jobs,
+ ::Types::Ci::JobType.connection_type,
+ null: true,
+ description: 'All jobs on this GitLab instance.',
+ resolver: ::Resolvers::Ci::AllJobsResolver
+
def design_management
DesignManagementObject.new(nil)
end
diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb
index ef701bbfc10..3b8f5c64beb 100644
--- a/app/graphql/types/subscription_type.rb
+++ b/app/graphql/types/subscription_type.rb
@@ -13,6 +13,9 @@ module Types
field :issuable_title_updated, subscription: Subscriptions::IssuableUpdated, null: true,
description: 'Triggered when the title of an issuable is updated.'
+ field :issuable_description_updated, subscription: Subscriptions::IssuableUpdated, null: true,
+ description: 'Triggered when the description of an issuable is updated.'
+
field :issuable_labels_updated, subscription: Subscriptions::IssuableUpdated, null: true,
description: 'Triggered when the labels of an issuable are updated.'
@@ -23,6 +26,11 @@ module Types
subscription: Subscriptions::IssuableUpdated,
null: true,
description: 'Triggered when the reviewers of a merge request are updated.'
+
+ field :merge_request_merge_status_updated,
+ subscription: Subscriptions::IssuableUpdated,
+ null: true,
+ description: 'Triggered when the merge status of a merge request is updated.'
end
end
diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb
index eca8c8d845a..a3943361114 100644
--- a/app/graphql/types/work_items/widget_interface.rb
+++ b/app/graphql/types/work_items/widget_interface.rb
@@ -23,6 +23,9 @@ module Types
ORPHAN_TYPES
end
+ # Whenever a new widget is added make sure to update the spec to avoid N + 1 queries in
+ # spec/requests/api/graphql/project/work_items_spec.rb and add the necessary preloads
+ # in app/graphql/resolvers/work_items_resolver.rb
def self.resolve_type(object, context)
case object
when ::WorkItems::Widgets::Description
diff --git a/app/graphql/types/work_items/widgets/labels_update_input_type.rb b/app/graphql/types/work_items/widgets/labels_update_input_type.rb
new file mode 100644
index 00000000000..d38b8cefa63
--- /dev/null
+++ b/app/graphql/types/work_items/widgets/labels_update_input_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ module Widgets
+ class LabelsUpdateInputType < BaseInputObject
+ graphql_name 'WorkItemWidgetLabelsUpdateInput'
+
+ argument :add_label_ids, [Types::GlobalIDType[::Label]],
+ required: false,
+ description: 'Global IDs of labels to be added to the work item.',
+ prepare: ->(label_ids, _ctx) { label_ids.map(&:model_id) }
+ argument :remove_label_ids, [Types::GlobalIDType[::Label]],
+ required: false,
+ description: 'Global IDs of labels to be removed from the work item.',
+ prepare: ->(label_ids, _ctx) { label_ids.map(&:model_id) }
+ end
+ end
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index a75c1b16145..32af1599bd1 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -313,7 +313,6 @@ module ApplicationHelper
class_names = []
class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards)
class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards)
- class_names << 'environment-logs-page' if current_controller?(:logs)
class_names << 'with-performance-bar' if performance_bar_enabled?
class_names << system_message_class
class_names << marketing_header_experiment_class
@@ -428,7 +427,7 @@ module ApplicationHelper
milestones: milestones_project_autocomplete_sources_path(object),
commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
snippets: snippets_project_autocomplete_sources_path(object),
- contacts: contacts_project_autocomplete_sources_path(object)
+ contacts: contacts_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
}
end
end
@@ -448,6 +447,10 @@ module ApplicationHelper
form_for(record, *(args << options.merge({ builder: ::Gitlab::FormBuilders::GitlabUiFormBuilder })), &block)
end
+ def gitlab_ui_form_with(**args, &block)
+ form_with(**args.merge({ builder: ::Gitlab::FormBuilders::GitlabUiFormBuilder }), &block)
+ end
+
private
def appearance
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index ddc682bc08a..21b18203677 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -441,7 +441,8 @@ module ApplicationSettingsHelper
:group_runner_token_expiration_interval,
:project_runner_token_expiration_interval,
:pipeline_limit_per_project_user_sha,
- :invitation_flow_enforcement
+ :invitation_flow_enforcement,
+ :can_create_group
].tap do |settings|
next if Gitlab.com?
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index f98e70e41d8..db6cf27566f 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -2,18 +2,15 @@
module BoardsHelper
def board
- @board ||= @board || @boards.first
+ @board
end
def board_data
{
- boards_endpoint: @boards_endpoint,
- lists_endpoint: board_lists_path(board),
board_id: board.id,
disabled: board.disabled_for?(current_user).to_s,
root_path: root_path,
full_path: full_path,
- bulk_update_path: @bulk_issues_path,
can_update: can_update?.to_s,
can_admin_list: can_admin_list?.to_s,
can_admin_board: can_admin_board?.to_s,
@@ -94,14 +91,6 @@ module BoardsHelper
!multiple_boards_available? && current_board_parent.boards.size > 1
end
- def current_board_path(board)
- @current_board_path ||= if board.group_board?
- group_board_path(current_board_parent, board)
- else
- project_board_path(current_board_parent, board)
- end
- end
-
def current_board_parent
@current_board_parent ||= @group || @project
end
@@ -121,18 +110,6 @@ module BoardsHelper
def can_admin_board?
can?(current_user, :admin_issue_board, current_board_parent)
end
-
- def can_admin_issue?
- can?(current_user, :admin_issue, current_board_parent)
- end
-
- def serializer
- CurrentBoardSerializer.new
- end
-
- def current_board_json
- serializer.represent(board).as_json
- end
end
BoardsHelper.prepend_mod_with('BoardsHelper')
diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb
index d00301678dd..99a92ba9b59 100644
--- a/app/helpers/ci/pipeline_editor_helper.rb
+++ b/app/helpers/ci/pipeline_editor_helper.rb
@@ -11,7 +11,6 @@ module Ci
def js_pipeline_editor_data(project)
initial_branch = params[:branch_name]
latest_commit = project.repository.commit(initial_branch) || project.commit
- commit_sha = latest_commit ? latest_commit.sha : ''
total_branches = project.repository_exists? ? project.repository.branch_count : 0
{
@@ -27,17 +26,26 @@ module Ci
"lint-unavailable-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'configuration-validation-currently-not-available-message'),
"needs-help-page-path" => help_page_path('ci/yaml/index', anchor: 'needs'),
"new-merge-request-path" => namespace_project_new_merge_request_path,
- "pipeline_etag" => latest_commit ? graphql_etag_pipeline_sha_path(commit_sha) : '',
+ "pipeline_etag" => latest_commit ? graphql_etag_pipeline_sha_path(latest_commit.sha) : '',
"pipeline-page-path" => project_pipelines_path(project),
"project-path" => project.path,
"project-full-path" => project.full_path,
"project-namespace" => project.namespace.full_path,
"simulate-pipeline-help-page-path" => help_page_path('ci/pipeline_editor/index', anchor: 'simulate-a-cicd-pipeline'),
"total-branches" => total_branches,
+ "uses-external-config" => uses_external_config?(project) ? 'true' : 'false',
"validate-tab-illustration-path" => image_path('illustrations/project-run-CICD-pipelines-sm.svg'),
"yml-help-page-path" => help_page_path('ci/yaml/index')
}
end
+
+ private
+
+ def uses_external_config?(project)
+ ci_config_source = Gitlab::Ci::ProjectConfig.new(project: project, sha: nil).source
+
+ [:external_project_source, :remote_source].include?(ci_config_source)
+ end
end
end
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index a67771116b9..c93c8dd8d76 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -69,7 +69,8 @@ module Ci
end
def has_pipeline_badges?(pipeline)
- pipeline.child? ||
+ pipeline.schedule? ||
+ pipeline.child? ||
pipeline.latest? ||
pipeline.merge_train_pipeline? ||
pipeline.has_yaml_errors? ||
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index e955ad4cfda..9ecf780f55b 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -42,7 +42,8 @@ module CompareHelper
source_project_refs_path: refs_project_path(project),
target_project_refs_path: refs_project_path(@target_project),
params_from: params[:from],
- params_to: params[:to]
+ params_to: params[:to],
+ straight: params[:straight]
}.tap do |data|
data[:projects_from] = target_projects(project).map do |target_project|
{ id: target_project.id, name: target_project.full_path }
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index bcddb889cf4..b717cbcc312 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -16,6 +16,54 @@ module EventsHelper
'joined' => 'users'
}.freeze
+ def localized_action_name_map
+ {
+ accepted: s_('Event|accepted'),
+ approved: s_('Event|approved'),
+ closed: s_('Event|closed'),
+ 'commented on': s_('Event|commented on'),
+ created: s_('Event|created'),
+ destroyed: s_('Event|destroyed'),
+ joined: s_('Event|joined'),
+ left: s_('Event|left'),
+ opened: s_('Event|opened'),
+ updated: s_('Event|updated'),
+ 'removed due to membership expiration from': s_('Event|removed due to membership expiration from')
+ }.merge(localized_push_action_name_map,
+ localized_created_project_action_name_map,
+ localized_design_action_names
+ ).freeze
+ end
+
+ def localized_push_action_name_map
+ {
+ 'pushed new': s_('Event|pushed new'),
+ deleted: s_('Event|deleted'),
+ 'pushed to': s_('Event|pushed to')
+ }.freeze
+ end
+
+ def localized_created_project_action_name_map
+ {
+ created: s_('Event|created'),
+ imported: s_('Event|imported')
+ }.freeze
+ end
+
+ def localized_design_action_names
+ {
+ added: s_('Event|added'),
+ updated: s_('Event|updated'),
+ removed: s_('Event|removed')
+ }.freeze
+ end
+
+ def localized_action_name(event)
+ action_name = event.action_name
+ # The action fallback is used to cover the types were not included in the maps.
+ localized_action_name_map[action_name.to_sym] || action_name
+ end
+
def link_to_author(event, self_added: false)
author = event.author
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index f2e24f54391..9e42aeea9ce 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -39,13 +39,13 @@ module FormHelper
end
end
- def dropdown_max_select(data)
- return data[:'max-select'] unless Feature.enabled?(:limit_reviewer_and_assignee_size)
+ def dropdown_max_select(data, feature_flag)
+ return data[:'max-select'] unless Feature.enabled?(feature_flag)
- if data[:'max-select'] && data[:'max-select'] < MergeRequest::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
+ if data[:'max-select'] && data[:'max-select'] < ::Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
data[:'max-select']
else
- MergeRequest::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
+ ::Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
end
end
@@ -117,10 +117,16 @@ module FormHelper
dropdown_data = multiple_reviewers_dropdown_options(dropdown_data)
end
+ dropdown_data[:data].merge!(reviewers_dropdown_options_for_suggested_reviewers)
dropdown_data
end
# Overwritten
+ def reviewers_dropdown_options_for_suggested_reviewers
+ {}
+ end
+
+ # Overwritten
def issue_supports_multiple_assignees?
false
end
@@ -156,7 +162,12 @@ module FormHelper
new_options[:title] = _('Select assignee(s)')
new_options[:data][:'dropdown-header'] = 'Assignee(s)'
- new_options[:data].delete(:'max-select')
+
+ if Feature.enabled?(:limit_assignees_per_issuable)
+ new_options[:data][:'max-select'] = ::Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
+ else
+ new_options[:data].delete(:'max-select')
+ end
new_options
end
@@ -168,7 +179,7 @@ module FormHelper
new_options[:data][:'dropdown-header'] = _('Reviewer(s)')
if Feature.enabled?(:limit_reviewer_and_assignee_size)
- new_options[:data][:'max-select'] = MergeRequest::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
+ new_options[:data][:'max-select'] = ::Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
else
new_options[:data].delete(:'max-select')
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index f77bd6621f9..6b00c213875 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -126,8 +126,8 @@ module GroupsHelper
group.root? && current_user.can?(:admin_setting_to_allow_project_access_token_creation, group)
end
- def show_thanks_for_purchase_alert?
- params.key?(:purchased_quantity) && params[:purchased_quantity].to_i > 0
+ def show_thanks_for_purchase_alert?(quantity)
+ quantity.to_i > 0
end
def project_list_sort_by
@@ -177,7 +177,8 @@ module GroupsHelper
subgroups_and_projects_endpoint: group_children_path(group, format: :json),
shared_projects_endpoint: group_shared_projects_path(group, format: :json),
archived_projects_endpoint: group_children_path(group, format: :json, archived: 'only'),
- current_group_visibility: group.visibility
+ current_group_visibility: group.visibility,
+ initial_sort: project_list_sort_by
}.merge(subgroups_and_projects_list_app_data(group))
end
diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb
index 1e50033e0e0..e050ccc0e40 100644
--- a/app/helpers/hooks_helper.rb
+++ b/app/helpers/hooks_helper.rb
@@ -1,6 +1,13 @@
# frozen_string_literal: true
module HooksHelper
+ def webhook_form_data(hook)
+ {
+ url: hook.url,
+ url_variables: nil
+ }
+ end
+
def link_to_test_hook(hook, trigger)
path = test_hook_path(hook, trigger)
trigger_human_name = trigger.to_s.tr('_', ' ').camelize
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index ec1327cf7ae..5b3ca25b5af 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -3,6 +3,32 @@
module IdeHelper
def ide_data
{
+ 'can-use-new-web-ide' => can_use_new_web_ide?.to_s,
+ 'use-new-web-ide' => use_new_web_ide?.to_s,
+ 'user-preferences-path' => profile_preferences_path,
+ 'branch-name' => @branch
+ }.merge(use_new_web_ide? ? new_ide_data : legacy_ide_data)
+ end
+
+ def can_use_new_web_ide?
+ Feature.enabled?(:vscode_web_ide, current_user)
+ end
+
+ def use_new_web_ide?
+ can_use_new_web_ide? && !current_user.use_legacy_web_ide
+ end
+
+ private
+
+ def new_ide_data
+ {
+ 'project-path' => @project&.path_with_namespace,
+ 'csp-nonce' => content_security_policy_nonce
+ }
+ end
+
+ def legacy_ide_data
+ {
'empty-state-svg-path' => image_path('illustrations/multi_file_editor_empty.svg'),
'no-changes-state-svg-path' => image_path('illustrations/multi-editor_no_changes_empty.svg'),
'committed-state-svg-path' => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
@@ -13,7 +39,6 @@ module IdeHelper
'clientside-preview-enabled': Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?.to_s,
'render-whitespace-in-code': current_user.render_whitespace_in_code.to_s,
'codesandbox-bundler-url': Gitlab::CurrentSettings.web_ide_clientside_preview_bundler_url,
- 'branch-name' => @branch,
'default-branch' => @project && @project.default_branch,
'file-path' => @path,
'merge-request' => @merge_request,
@@ -24,13 +49,10 @@ module IdeHelper
'web-terminal-svg-path' => image_path('illustrations/web-ide_promotion.svg'),
'web-terminal-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'interactive-web-terminals-for-the-web-ide'),
'web-terminal-config-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'web-ide-configuration-file'),
- 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration'),
- 'csp-nonce' => content_security_policy_nonce
+ 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration')
}
end
- private
-
def convert_to_project_entity_json(project)
return unless project
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 96daf398243..2804a58da9e 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -157,9 +157,9 @@ module IssuablesHelper
if issuable.respond_to?(:work_item_type) && WorkItems::Type::WI_TYPES_WITH_CREATED_HEADER.include?(issuable.work_item_type.base_type)
output << content_tag(:span, sprite_icon("#{issuable.work_item_type.icon_name}", css_class: 'gl-icon gl-vertical-align-middle gl-text-gray-500'), class: 'gl-mr-2', aria: { hidden: 'true' })
- output << s_('IssuableStatus|%{wi_type} created %{created_at} by ').html_safe % { wi_type: issuable.issue_type.capitalize, created_at: time_ago_with_tooltip(issuable.created_at) }
+ output << content_tag(:span, s_('IssuableStatus|%{wi_type} created %{created_at} by ').html_safe % { wi_type: issuable.issue_type.capitalize, created_at: time_ago_with_tooltip(issuable.created_at) }, class: 'gl-mr-2' )
else
- output << s_('IssuableStatus|Created %{created_at} by').html_safe % { created_at: time_ago_with_tooltip(issuable.created_at) }
+ output << content_tag(:span, s_('IssuableStatus|Created %{created_at} by').html_safe % { created_at: time_ago_with_tooltip(issuable.created_at) }, class: 'gl-mr-2' )
end
if issuable.is_a?(Issue) && issuable.service_desk_reply_to
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index a157b1b7b21..115cdd432e3 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -3,6 +3,10 @@
module IssuesHelper
include Issues::IssueTypeHelpers
+ def can_admin_issue?
+ can?(current_user, :admin_issue, @group || @project)
+ end
+
def issue_css_classes(issue)
classes = ["issue"]
classes << "closed" if issue.closed?
@@ -11,6 +15,11 @@ module IssuesHelper
classes.join(' ')
end
+ def show_timeline_view_toggle?(issue)
+ # Overridden in EE
+ false
+ end
+
def issue_manual_ordering_class
is_sorting_by_relative_position = @sort == 'relative_position'
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index fc558958ca3..866399f3021 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -12,22 +12,6 @@ module MarkupHelper
# https://gitlab.com/gitlab-org/gitlab/-/issues/365358
RENDER_TIMEOUT = 5.seconds
- def plain?(filename)
- Gitlab::MarkupHelper.plain?(filename)
- end
-
- def markup?(filename)
- Gitlab::MarkupHelper.markup?(filename)
- end
-
- def gitlab_markdown?(filename)
- Gitlab::MarkupHelper.gitlab_markdown?(filename)
- end
-
- def asciidoc?(filename)
- Gitlab::MarkupHelper.asciidoc?(filename)
- end
-
# Use this in places where you would normally use link_to(gfm(...), ...).
def link_to_markdown(body, url, html_options = {})
return '' if body.blank?
@@ -88,8 +72,10 @@ module MarkupHelper
tags = %w(a gl-emoji b strong i em pre code p span)
tags << 'img' if options[:allow_images]
- text = truncate_visible(md, max_chars || md.length)
- text = prepare_for_rendering(text, markdown_field_render_context(object, attribute, options))
+ context = markdown_field_render_context(object, attribute, options)
+ context.reverse_merge!(truncate_visible_max_chars: max_chars || md.length)
+
+ text = prepare_for_rendering(md, context)
text = sanitize(
text,
tags: tags,
@@ -146,11 +132,11 @@ module MarkupHelper
return '' unless text.present?
markup = proc do
- if gitlab_markdown?(file_name)
+ if Gitlab::MarkupHelper.gitlab_markdown?(file_name)
markdown_unsafe(text, context)
- elsif asciidoc?(file_name)
+ elsif Gitlab::MarkupHelper.asciidoc?(file_name)
asciidoc_unsafe(text, context)
- elsif plain?(file_name)
+ elsif Gitlab::MarkupHelper.plain?(file_name)
plain_unsafe(text)
else
other_markup_unsafe(file_name, text, context)
@@ -207,55 +193,6 @@ module MarkupHelper
{ project: wiki.container }
end
- # Return +text+, truncated to +max_chars+ characters, excluding any HTML
- # tags.
- def truncate_visible(text, max_chars)
- doc = Nokogiri::HTML.fragment(text)
- content_length = 0
- truncated = false
-
- doc.traverse do |node|
- if node.text? || node.content.empty?
- if truncated
- node.remove
- next
- end
-
- # Handle line breaks within a node
- if node.content.strip.lines.length > 1
- node.content = "#{node.content.lines.first.chomp}..."
- truncated = true
- end
-
- num_remaining = max_chars - content_length
- if node.content.length > num_remaining
- node.content = node.content.truncate(num_remaining)
- truncated = true
- end
-
- content_length += node.content.length
- end
-
- truncated = truncate_if_block(node, truncated)
- end
-
- doc.to_html
- end
-
- # Used by #truncate_visible. If +node+ is the first block element, and the
- # text hasn't already been truncated, then append "..." to the node contents
- # and return true. Otherwise return false.
- def truncate_if_block(node, truncated)
- return true if truncated
-
- if node.element? && (node.description&.block? || node.matches?('pre > code > .line'))
- node.inner_html = "#{node.inner_html}..." if node.next_sibling
- true
- else
- truncated
- end
- end
-
def strip_empty_link_tags(text)
scrubber = Loofah::Scrubber.new do |node|
node.remove if node.name == 'a' && node.children.empty?
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
new file mode 100644
index 00000000000..272a3970bc2
--- /dev/null
+++ b/app/helpers/milestones_helper.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module MilestonesHelper
+ def milestone_header_class(primary, issuables)
+ header_color = milestone_header_color(primary: primary)
+ header_border = milestone_header_border(issuables)
+
+ "#{header_color} #{header_border} gl-display-flex"
+ end
+
+ def milestone_counter_class(primary)
+ primary ? 'gl-text-white' : 'gl-text-gray-500'
+ end
+
+ private
+
+ def milestone_header_color(primary: false)
+ return '' unless primary
+
+ 'gl-bg-blue-500 gl-text-white'
+ end
+
+ def milestone_header_border(issuables)
+ issuables.empty? ? 'gl-border-bottom-0 gl-rounded-base' : ''
+ end
+end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 4d6ab7b8bf9..0cf2c5cea4c 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -49,7 +49,7 @@ module NavHelper
end
def page_has_markdown?
- current_path?('merge_requests#show') ||
+ current_path?('projects/merge_requests#show') ||
current_path?('projects/merge_requests/conflicts#show') ||
current_path?('issues#show') ||
current_path?('milestones#show') ||
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index e760fad7be9..cddcdf77710 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -471,8 +471,24 @@ module ProjectsHelper
}
end
+ def localized_project_human_access(access)
+ localized_access_names[access] || Gitlab::Access.human_access(access)
+ end
+
private
+ def localized_access_names
+ {
+ Gitlab::Access::NO_ACCESS => _('No access'),
+ Gitlab::Access::MINIMAL_ACCESS => _("Minimal Access"),
+ Gitlab::Access::GUEST => _('Guest'),
+ Gitlab::Access::REPORTER => _('Reporter'),
+ Gitlab::Access::DEVELOPER => _('Developer'),
+ Gitlab::Access::MAINTAINER => _('Maintainer'),
+ Gitlab::Access::OWNER => _('Owner')
+ }
+ end
+
def configure_oauth_import_message(provider, help_url)
str = if current_user.admin?
'ImportProjects|To enable importing projects from %{provider}, as administrator you need to configure %{link_start}OAuth integration%{link_end}'
diff --git a/app/helpers/recaptcha_helper.rb b/app/helpers/recaptcha_helper.rb
index 5b17ab4b815..59f0dc8f819 100644
--- a/app/helpers/recaptcha_helper.rb
+++ b/app/helpers/recaptcha_helper.rb
@@ -2,9 +2,27 @@
module RecaptchaHelper
def recaptcha_enabled?
+ return false if gitlab_qa?
+
!!Gitlab::Recaptcha.enabled?
end
alias_method :show_recaptcha_sign_up?, :recaptcha_enabled?
+
+ def recaptcha_enabled_on_login?
+ return false if gitlab_qa?
+
+ Gitlab::Recaptcha.enabled_on_login?
+ end
+
+ private
+
+ def gitlab_qa?
+ return false unless Gitlab.com?
+ return false unless request.user_agent.present?
+ return false unless Gitlab::Environment.qa_user_agent.present?
+
+ ActiveSupport::SecurityUtils.secure_compare(request.user_agent, Gitlab::Environment.qa_user_agent)
+ end
end
RecaptchaHelper.prepend_mod
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index 50089c7edab..e0db40ebaee 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -82,7 +82,7 @@ module ReleasesHelper
markdown_docs_path: help_page_path('user/markdown'),
release_assets_docs_path: releases_help_page_path(anchor: 'release-assets'),
manage_milestones_path: project_milestones_path(@project),
- new_milestone_path: new_project_milestone_path(@project),
+ new_milestone_path: new_project_milestone_path(@project, redirect_path: 'new_release'),
edit_release_docs_path: releases_help_page_path(anchor: 'edit-a-release'),
upcoming_release_docs_path: releases_help_page_path(anchor: 'upcoming-releases')
}
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index b16235893ae..f2b88287277 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -380,6 +380,40 @@ module SearchHelper
end
end
+ def search_filter_link_json(scope, label, data, search)
+ scope_name = scope.to_s
+ search_params = params.merge(search).merge({ scope: scope_name }).permit(SEARCH_GENERIC_PARAMS)
+ active_scope = @scope == scope_name
+
+ result = { label: label, scope: scope_name, data: data, link: search_path(search_params), active: active_scope }
+ result[:count] = @search_results.formatted_count(scope_name) if active_scope && !@timeout
+ result[:count_link] = search_count_path(search_params) unless active_scope
+
+ result
+ end
+
+ # search page scope navigation
+ def search_navigation
+ {
+ projects: { label: _("Projects"), data: { qa_selector: 'projects_tab' }, condition: @project.nil? },
+ blobs: { label: _("Code"), data: { qa_selector: 'code_tab' }, condition: project_search_tabs?(:blobs) || search_service.show_elasticsearch_tabs? || feature_flag_tab_enabled?(:global_search_code_tab) },
+ issues: { label: _("Issues"), condition: project_search_tabs?(:issues) || feature_flag_tab_enabled?(:global_search_issues_tab) },
+ merge_requests: { label: _("Merge requests"), condition: project_search_tabs?(:merge_requests) || feature_flag_tab_enabled?(:global_search_merge_requests_tab) },
+ wiki_blobs: { label: _("Wiki"), condition: project_search_tabs?(:wiki) || search_service.show_elasticsearch_tabs? },
+ commits: { label: _("Commits"), condition: project_search_tabs?(:commits) || (search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_commits_tab)) },
+ notes: { label: _("Comments"), condition: project_search_tabs?(:notes) || search_service.show_elasticsearch_tabs? },
+ milestones: { label: _("Milestones"), condition: project_search_tabs?(:milestones) || @project.nil? },
+ users: { label: _("Users"), condition: show_user_search_tab? },
+ snippet_titles: { label: _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }, condition: @show_snippets.present? && @project.nil? }
+ }
+ end
+
+ def search_navigation_json
+ search_navigation.each_with_object({}) do |(key, value), hash|
+ hash[key] = search_filter_link_json(key, value[:label], value[:data], value[:search]) if value[:condition]
+ end.to_json
+ end
+
def search_filter_input_options(type, placeholder = _('Search or filter results...'))
opts =
{
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 88aff31af54..14ee6007a43 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -39,11 +39,6 @@ module SelectsHelper
select2_tag(id, opts)
end
- def namespace_select_tag(id, opts = {})
- opts[:class] = [*opts[:class], 'ajax-namespace-select'].join(' ')
- select2_tag(id, opts)
- end
-
def project_select_tag(id, opts = {})
opts[:class] = [*opts[:class], 'ajax-project-select'].join(' ')
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
index 129180d1ccf..56138ba95c2 100644
--- a/app/helpers/sessions_helper.rb
+++ b/app/helpers/sessions_helper.rb
@@ -49,6 +49,6 @@ module SessionsHelper
match = regex.match(email)
return email unless match
- match[1] + '*' * match[2].length + match[3] + '*' * match[4].length + match[5]
+ match[1] + '*' * (match[2] || '').length + match[3] + '*' * (match[4] || '').length + match[5]
end
end
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index ecedbfb2a4f..cb6f60ab79b 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -8,12 +8,12 @@ module TimeHelper
if minutes >= 1
if seconds % 60 == 0
- pluralize(minutes, "minute")
+ n_('%d minute', '%d minutes', minutes) % minutes
else
- [pluralize(minutes, "minute"), pluralize(seconds, "second")].to_sentence
+ [n_('%d minute', '%d minutes', minutes) % minutes, n_('%d second', '%d seconds', seconds) % seconds].to_sentence
end
else
- pluralize(seconds, "second")
+ n_('%d second', '%d seconds', seconds) % seconds
end
end
diff --git a/app/helpers/timeboxes_helper.rb b/app/helpers/timeboxes_helper.rb
index 11d09a79dcf..e0e6229bc6d 100644
--- a/app/helpers/timeboxes_helper.rb
+++ b/app/helpers/timeboxes_helper.rb
@@ -77,14 +77,10 @@ module TimeboxesHelper
end
def milestone_progress_bar(milestone)
- options = {
- class: 'progress-bar bg-success',
- style: "width: #{milestone.percent_complete}%;"
- }
-
- content_tag :div, class: 'progress' do
- content_tag :div, nil, options
- end
+ render Pajamas::ProgressComponent.new(
+ value: milestone.percent_complete,
+ variant: :success
+ )
end
def milestone_time_for(date, date_type)
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index ecf29c41100..520cde9ecee 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -130,12 +130,12 @@ module TodosHelper
def todos_filter_params
{
- state: params[:state],
+ state: params[:state].presence,
project_id: params[:project_id],
author_id: params[:author_id],
type: params[:type],
action_id: params[:action_id]
- }
+ }.compact
end
def todos_filter_empty?
@@ -168,22 +168,22 @@ module TodosHelper
def todo_actions_options
[
- { id: '', text: 'Any Action' },
- { id: Todo::ASSIGNED, text: 'Assigned' },
- { id: Todo::REVIEW_REQUESTED, text: 'Review requested' },
- { id: Todo::MENTIONED, text: 'Mentioned' },
- { id: Todo::MARKED, text: 'Added' },
- { id: Todo::BUILD_FAILED, text: 'Pipelines' }
+ { id: '', text: s_('Todos|Any Action') },
+ { id: Todo::ASSIGNED, text: s_('Todos|Assigned') },
+ { id: Todo::REVIEW_REQUESTED, text: s_('Todos|Review requested') },
+ { id: Todo::MENTIONED, text: s_('Todos|Mentioned') },
+ { id: Todo::MARKED, text: s_('Todos|Added') },
+ { id: Todo::BUILD_FAILED, text: s_('Todos|Pipelines') }
]
end
def todo_types_options
[
- { id: '', text: 'Any Type' },
- { id: 'Issue', text: 'Issue' },
- { id: 'MergeRequest', text: 'Merge request' },
- { id: 'DesignManagement::Design', text: 'Design' },
- { id: 'AlertManagement::Alert', text: 'Alert' }
+ { id: '', text: s_('Todos|Any Type') },
+ { id: 'Issue', text: s_('Todos|Issue') },
+ { id: 'MergeRequest', text: s_('Todos|Merge request') },
+ { id: 'DesignManagement::Design', text: s_('Todos|Design') },
+ { id: 'AlertManagement::Alert', text: s_('Todos|Alert') }
]
end
diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb
index d8baa185370..a9fd219bbac 100644
--- a/app/helpers/users/callouts_helper.rb
+++ b/app/helpers/users/callouts_helper.rb
@@ -76,8 +76,8 @@ module Users
user_dismissed?(WEB_HOOK_DISABLED, last_failure, project: project)
end
- def show_merge_request_settings_callout?
- !user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT)
+ def show_merge_request_settings_callout?(project)
+ !user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT) && project.merge_requests_enabled?
end
def ultimate_feature_removal_banner_dismissed?(project)
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 271fa47dd97..4f345fdeb9c 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -287,6 +287,21 @@ module UsersHelper
}
]
end
+
+ # the keys should match the user model defined roles in app/models/user.rb
+ def localized_user_roles
+ {
+ software_developer: s_('User|Software Developer'),
+ development_team_lead: s_('User|Development Team Lead'),
+ devops_engineer: s_('User|Devops Engineer'),
+ systems_administrator: s_('User|Systems Administrator'),
+ security_analyst: s_('User|Security Analyst'),
+ data_analyst: s_('User|Data Analyst'),
+ product_manager: s_('User|Product Manager'),
+ product_designer: s_('User|Product Designer'),
+ other: s_('User|Other')
+ }.with_indifferent_access.freeze
+ end
end
UsersHelper.prepend_mod_with('UsersHelper')
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index d6ffd3deafe..017a1861905 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -59,14 +59,16 @@ module WikiHelper
end
end
- def wiki_sort_controls(wiki, sort, direction)
- sort ||= Wiki::TITLE_ORDER
+ def wiki_sort_controls(wiki, direction)
link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort'
reversed_direction = direction == 'desc' ? 'asc' : 'desc'
icon_class = direction == 'desc' ? 'highest' : 'lowest'
+ title = direction == 'desc' ? _('Sort direction: Descending') : _('Sort direction: Ascending')
- link_to(wiki_path(wiki, action: :pages, sort: sort, direction: reversed_direction),
- type: 'button', class: link_class, title: _('Sort direction')) do
+ link_options = { action: :pages, direction: reversed_direction }
+
+ link_to(wiki_path(wiki, **link_options),
+ type: 'button', class: link_class, title: title) do
sprite_icon("sort-#{icon_class}")
end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 8fe471a48f2..65ea90d0b5d 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -94,6 +94,18 @@ module Emails
end
end
+ def access_token_revoked_email(user, token_name)
+ return unless user&.active?
+
+ @user = user
+ @token_name = token_name
+ @target_url = profile_personal_access_tokens_url
+
+ Gitlab::I18n.with_locale(@user.preferred_language) do
+ mail_with_locale(to: @user.notification_email_or_default, subject: subject(_("A personal access token has been revoked")))
+ end
+ end
+
def ssh_key_expired_email(user, fingerprints)
return unless user&.active?
@@ -131,6 +143,18 @@ module Emails
end
end
+ def two_factor_otp_attempt_failed_email(user, ip, time = Time.current)
+ @user = user
+ @ip = ip
+ @time = time
+
+ Gitlab::I18n.with_locale(@user.preferred_language) do
+ email_with_layout(
+ to: @user.notification_email_or_default,
+ subject: subject(_("Attempted sign in to %{host} using a wrong two-factor authentication code") % { host: Gitlab.config.gitlab.host }))
+ end
+ end
+
def disabled_two_factor_email(user)
return unless user
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index c5e60ecaadd..206518e582b 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -181,6 +181,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.unknown_sign_in_email(user, '127.0.0.1', Time.current).message
end
+ def two_factor_otp_attempt_failed_email
+ Notify.two_factor_otp_attempt_failed_email(user, '127.0.0.1').message
+ end
+
def new_email_address_added_email
Notify.new_email_address_added_email(user, 'someone@gitlab.com').message
end
@@ -209,6 +213,18 @@ class NotifyPreview < ActionMailer::Preview
Notify.verification_instructions_email(user.id, token: '123456', expires_in: 60).message
end
+ def project_was_exported_email
+ Notify.project_was_exported_email(user, project).message
+ end
+
+ def request_review_merge_request_email
+ Notify.request_review_merge_request_email(user.id, merge_request.id, user.id).message
+ end
+
+ def project_was_moved_email
+ Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab").message
+ end
+
private
def project
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index edb9a2053b1..361b1a8dca9 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -120,7 +120,7 @@ class ApplicationSetting < ApplicationRecord
if: :help_page_support_url_column_exists?
validates :help_page_documentation_base_url,
- length: { maximum: 255, message: _("is too long (maximum is %{count} characters)") },
+ length: { maximum: 255, message: N_("is too long (maximum is %{count} characters)") },
allow_blank: true,
addressable_url: true
@@ -148,7 +148,7 @@ class ApplicationSetting < ApplicationRecord
if: :akismet_enabled
validates :spam_check_api_key,
- length: { maximum: 2000, message: _('is too long (maximum is %{count} characters)') },
+ length: { maximum: 2000, message: N_('is too long (maximum is %{count} characters)') },
allow_blank: true
validates :unique_ips_limit_per_user,
@@ -228,7 +228,7 @@ class ApplicationSetting < ApplicationRecord
validates :default_artifacts_expire_in, presence: true, duration: true
validates :container_expiration_policies_enable_historic_entries,
- inclusion: { in: [true, false], message: _('must be a boolean value') }
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
validates :container_registry_token_expire_delay,
presence: true,
@@ -320,8 +320,8 @@ class ApplicationSetting < ApplicationRecord
validates :personal_access_token_prefix,
format: { with: %r{\A[a-zA-Z0-9_+=/@:.-]+\z},
- message: _("can contain only letters of the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'") },
- length: { maximum: 20, message: _('is too long (maximum is %{count} characters)') },
+ message: N_("can contain only letters of the Base64 alphabet (RFC4648) with the addition of '@', ':' and '.'") },
+ length: { maximum: 20, message: N_('is too long (maximum is %{count} characters)') },
allow_blank: true
validates :commit_email_hostname, format: { with: /\A[^@]+\z/ }
@@ -369,7 +369,7 @@ class ApplicationSetting < ApplicationRecord
validates :email_restrictions, untrusted_regexp: true
- validates :hashed_storage_enabled, inclusion: { in: [true], message: _("Hashed storage can't be disabled anymore for new projects") }
+ validates :hashed_storage_enabled, inclusion: { in: [true], message: N_("Hashed storage can't be disabled anymore for new projects") }
validates :container_registry_delete_tags_service_timeout,
:container_registry_cleanup_tags_service_max_list_size,
@@ -377,7 +377,7 @@ class ApplicationSetting < ApplicationRecord
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :container_registry_expiration_policies_caching,
- inclusion: { in: [true, false], message: _('must be a boolean value') }
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
validates :container_registry_import_max_tags_count,
:container_registry_import_max_retries,
@@ -404,11 +404,18 @@ class ApplicationSetting < ApplicationRecord
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :invisible_captcha_enabled,
- inclusion: { in: [true, false], message: _('must be a boolean value') }
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
- validates :invitation_flow_enforcement,
+ validates :invitation_flow_enforcement, :can_create_group,
allow_nil: false,
- inclusion: { in: [true, false], message: _('must be a boolean value') }
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
+
+ # rubocop:disable Cop/StaticTranslationDefinition
+ validates :deactivate_dormant_users_period,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 90, message: _("'%{value}' days of inactivity must be greater than or equal to 90") },
+ if: :deactivate_dormant_users?
+ # rubocop:enable Cop/StaticTranslationDefinition
Gitlab::SSHPublicKey.supported_types.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
@@ -513,11 +520,11 @@ class ApplicationSetting < ApplicationRecord
rsa_key: true, allow_nil: true
validates :rate_limiting_response_text,
- length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') },
+ length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') },
allow_blank: true
validates :jira_connect_application_key,
- length: { maximum: 255, message: _('is too long (maximum is %{count} characters)') },
+ length: { maximum: 255, message: N_('is too long (maximum is %{count} characters)') },
allow_blank: true
with_options(presence: true, numericality: { only_integer: true, greater_than: 0 }) do
@@ -561,7 +568,7 @@ class ApplicationSetting < ApplicationRecord
allow_nil: false
validates :admin_mode,
- inclusion: { in: [true, false], message: _('must be a boolean value') }
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
validates :external_pipeline_validation_service_url,
addressable_url: true, allow_blank: true
@@ -574,7 +581,7 @@ class ApplicationSetting < ApplicationRecord
inclusion: { in: ApplicationSetting.whats_new_variants.keys }
validates :floc_enabled,
- inclusion: { in: [true, false], message: _('must be a boolean value') }
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
enum sidekiq_job_limiter_mode: {
Gitlab::SidekiqMiddleware::SizeLimiter::Validator::TRACK_MODE => 0,
@@ -589,7 +596,7 @@ class ApplicationSetting < ApplicationRecord
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :sentry_enabled,
- inclusion: { in: [true, false], message: _('must be a boolean value') }
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
validates :sentry_dsn,
addressable_url: true, presence: true, length: { maximum: 255 },
if: :sentry_enabled?
@@ -601,7 +608,7 @@ class ApplicationSetting < ApplicationRecord
if: :sentry_enabled?
validates :error_tracking_enabled,
- inclusion: { in: [true, false], message: _('must be a boolean value') }
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
validates :error_tracking_api_url,
presence: true,
addressable_url: true,
@@ -667,9 +674,10 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :arkose_labs_public_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :arkose_labs_private_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :cube_api_key, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :jitsu_administrator_password, encryption_options_base_32_aes_256_gcm
validates :disable_feed_token,
- inclusion: { in: [true, false], message: _('must be a boolean value') }
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
@@ -791,6 +799,10 @@ class ApplicationSetting < ApplicationRecord
::AsciidoctorExtensions::Kroki::SUPPORTED_DIAGRAM_NAMES.include?(diagram_type)
end
+ def personal_access_tokens_disabled?
+ false
+ end
+
private
def parsed_grafana_url
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 4d377855dea..dee4bd07fd9 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -240,7 +240,8 @@ module ApplicationSettingImplementation
search_rate_limit: 30,
search_rate_limit_unauthenticated: 10,
users_get_by_id_limit: 300,
- users_get_by_id_limit_allowlist: []
+ users_get_by_id_limit_allowlist: [],
+ can_create_group: true
}
end
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 5430575ace7..e9530a80d9f 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -73,4 +73,8 @@ class AwardEmoji < ApplicationRecord
awardable.expire_etag_cache if awardable.is_a?(Note)
awardable.try(:update_upvotes_count) if upvote?
end
+
+ def to_ability_name
+ 'emoji'
+ end
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index e0a616b5fb4..a2542e669e1 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -116,8 +116,20 @@ class BulkImports::Entity < ApplicationRecord
"/#{pluralized_name}/#{encoded_source_full_path}"
end
+ def base_xid_resource_url_path
+ "/#{pluralized_name}/#{source_xid}"
+ end
+
+ def base_resource_path
+ if source_xid.present?
+ base_xid_resource_url_path
+ else
+ base_resource_url_path
+ end
+ end
+
def export_relations_url_path
- "#{base_resource_url_path}/export_relations"
+ "#{base_resource_path}/export_relations"
end
def relation_download_url_path(relation)
@@ -125,7 +137,7 @@ class BulkImports::Entity < ApplicationRecord
end
def wikis_url_path
- "#{base_resource_url_path}/wikis"
+ "#{base_resource_path}/wikis"
end
def project?
@@ -149,6 +161,13 @@ class BulkImports::Entity < ApplicationRecord
end
def validate_imported_entity_type
+ if project_entity? && !BulkImports::Features.project_migration_enabled?(destination_namespace)
+ errors.add(
+ :base,
+ s_('BulkImport|invalid entity source type')
+ )
+ end
+
if group.present? && project_entity?
errors.add(
:group,
diff --git a/app/models/bulk_imports/export_status.rb b/app/models/bulk_imports/export_status.rb
index 4fea62edb2a..cbd7b189007 100644
--- a/app/models/bulk_imports/export_status.rb
+++ b/app/models/bulk_imports/export_status.rb
@@ -30,14 +30,18 @@ module BulkImports
private
- attr_reader :client, :entity, :relation
+ attr_reader :client, :entity, :relation, :pipeline_tracker
def export_status
strong_memoize(:export_status) do
fetch_export_status&.find { |item| item['relation'] == relation }
+ rescue BulkImports::NetworkError => e
+ raise BulkImports::RetryPipelineError.new(e.message, 2.seconds) if e.retriable?(pipeline_tracker)
+
+ default_error_response(e.message)
+ rescue StandardError => e
+ default_error_response(e.message)
end
- rescue StandardError => e
- { 'status' => Export::FAILED, 'error' => e.message }
end
def fetch_export_status
@@ -47,5 +51,9 @@ module BulkImports
def status_endpoint
File.join(entity.export_relations_url_path, 'status')
end
+
+ def default_error_response(message)
+ { 'status' => Export::FAILED, 'error' => message }
+ end
end
end
diff --git a/app/models/bulk_imports/failure.rb b/app/models/bulk_imports/failure.rb
index a6f7582c3b0..44d16618c77 100644
--- a/app/models/bulk_imports/failure.rb
+++ b/app/models/bulk_imports/failure.rb
@@ -10,4 +10,24 @@ class BulkImports::Failure < ApplicationRecord
optional: false
validates :entity, presence: true
+
+ def relation
+ pipeline_relation || default_relation
+ end
+
+ private
+
+ def pipeline_relation
+ klass = pipeline_class.constantize
+
+ return unless klass.ancestors.include?(BulkImports::Pipeline)
+
+ klass.relation
+ rescue NameError
+ nil
+ end
+
+ def default_relation
+ pipeline_class.demodulize.chomp('Pipeline').underscore
+ end
end
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index fa38b7617d2..357f4629078 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -60,6 +60,8 @@ class BulkImports::Tracker < ApplicationRecord
event :retry do
transition started: :enqueued
+ # To avoid errors when retrying a pipeline in case of network errors
+ transition enqueued: :enqueued
end
event :enqueue do
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 4e58f877217..b8511536e32 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -108,10 +108,12 @@ module Ci
validates :ref, presence: true
scope :not_interruptible, -> do
- joins(:metadata).where.not('ci_builds_metadata.id' => Ci::BuildMetadata.scoped_build.with_interruptible.select(:id))
+ joins(:metadata)
+ .where.not(Ci::BuildMetadata.table_name => { id: Ci::BuildMetadata.scoped_build.with_interruptible.select(:id) })
end
scope :unstarted, -> { where(runner_id: nil) }
+
scope :with_downloadable_artifacts, -> do
where('EXISTS (?)',
Ci::JobArtifact.select(1)
@@ -120,6 +122,14 @@ module Ci
)
end
+ scope :with_erasable_artifacts, -> do
+ where('EXISTS (?)',
+ Ci::JobArtifact.select(1)
+ .where('ci_builds.id = ci_job_artifacts.job_id')
+ .where(file_type: Ci::JobArtifact.erasable_file_types)
+ )
+ end
+
scope :in_pipelines, ->(pipelines) do
where(pipeline: pipelines)
end
@@ -178,7 +188,7 @@ module Ci
scope :license_management_jobs, -> { where(name: %i(license_management license_scanning)) } # handle license rename https://gitlab.com/gitlab-org/gitlab/issues/8911
scope :with_secure_reports_from_config_options, -> (job_types) do
- joins(:metadata).where("ci_builds_metadata.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types)
+ joins(:metadata).where("#{Ci::BuildMetadata.quoted_table_name}.config_options -> 'artifacts' -> 'reports' ?| array[:job_types]", job_types: job_types)
end
scope :with_coverage, -> { where.not(coverage: nil) }
@@ -218,7 +228,7 @@ module Ci
yaml_variables when environment coverage_regex
description tag_list protected needs_attributes
job_variables_attributes resource_group scheduling_type
- ci_stage partition_id].freeze
+ ci_stage partition_id id_tokens].freeze
end
end
@@ -407,18 +417,10 @@ module Ci
pipeline.manual_actions.reject { |action| action.name == self.name }
end
- def environment_manual_actions
- pipeline.manual_actions.filter { |action| action.expanded_environment_name == self.expanded_environment_name }
- end
-
def other_scheduled_actions
pipeline.scheduled_actions.reject { |action| action.name == self.name }
end
- def environment_scheduled_actions
- pipeline.scheduled_actions.filter { |action| action.expanded_environment_name == self.expanded_environment_name }
- end
-
def pages_generator?
Gitlab.config.pages.enabled &&
self.name == 'pages'
@@ -445,8 +447,7 @@ module Ci
def prevent_rollback_deployment?
strong_memoize(:prevent_rollback_deployment) do
- Feature.enabled?(:prevent_outdated_deployment_jobs, project) &&
- starts_environment? &&
+ starts_environment? &&
project.ci_forward_deployment_enabled? &&
deployment&.older_than_last_successful_deployment?
end
@@ -1195,6 +1196,14 @@ module Ci
end
def job_jwt_variables
+ if project.ci_cd_settings.opt_in_jwt?
+ id_tokens_variables
+ else
+ legacy_jwt_variables.concat(id_tokens_variables)
+ end
+ end
+
+ def legacy_jwt_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless Feature.enabled?(:ci_job_jwt, project)
@@ -1208,6 +1217,20 @@ module Ci
end
end
+ def id_tokens_variables
+ return [] unless id_tokens?
+
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ id_tokens.each do |var_name, token_data|
+ token = Gitlab::Ci::JwtV2.for_build(self, aud: token_data['id_token']['aud'])
+
+ variables.append(key: var_name, value: token, public: false, masked: true)
+ end
+ rescue OpenSSL::PKey::RSAError, Gitlab::Ci::Jwt::NoSigningKeyError => e
+ Gitlab::ErrorTracking.track_exception(e)
+ end
+ end
+
def cache_for_online_runners(&block)
Rails.cache.fetch(
['has-online-runners', id],
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 3bdf2f90acb..33092e881f0 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -6,11 +6,14 @@ module Ci
class BuildMetadata < Ci::ApplicationRecord
BuildTimeout = Struct.new(:value, :source)
+ include Ci::Partitionable
include Presentable
include ChronicDurationAttribute
include Gitlab::Utils::StrongMemoize
self.table_name = 'ci_builds_metadata'
+ self.primary_key = 'id'
+ partitionable scope: :build
belongs_to :build, class_name: 'CommitStatus'
belongs_to :project
@@ -27,7 +30,7 @@ module Ci
chronic_duration_attr_reader :timeout_human_readable, :timeout
- scope :scoped_build, -> { where('ci_builds_metadata.build_id = ci_builds.id') }
+ scope :scoped_build, -> { where("#{quoted_table_name}.build_id = #{Ci::Build.quoted_table_name}.id") }
scope :with_interruptible, -> { where(interruptible: true) }
scope :with_exposed_artifacts, -> { where(has_exposed_artifacts: true) }
diff --git a/app/models/ci/job_token/project_scope_link.rb b/app/models/ci/job_token/project_scope_link.rb
index c2ab8ca0929..3fdf07123e6 100644
--- a/app/models/ci/job_token/project_scope_link.rb
+++ b/app/models/ci/job_token/project_scope_link.rb
@@ -19,6 +19,11 @@ module Ci
validates :target_project, presence: true
validate :not_self_referential_link
+ enum direction: {
+ outbound: 0,
+ inbound: 1
+ }
+
def self.for_source_and_target(source_project, target_project)
self.find_by(source_project: source_project, target_project: target_project)
end
diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb
index 26a49d6a730..1aa49b95201 100644
--- a/app/models/ci/job_token/scope.rb
+++ b/app/models/ci/job_token/scope.rb
@@ -23,7 +23,7 @@ module Ci
def includes?(target_project)
# if the setting is disabled any project is considered to be in scope.
- return true unless source_project.ci_job_token_scope_enabled?
+ return true unless source_project.ci_outbound_job_token_scope_enabled?
target_project.id == source_project.id ||
Ci::JobToken::ProjectScopeLink.from_project(source_project).to_project(target_project).exists?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 1e328c3c573..950e0a583bc 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -112,6 +112,8 @@ module Ci
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
+ has_one :pipeline_metadata, class_name: 'Ci::PipelineMetadata', inverse_of: :pipeline
+
has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult', foreign_key: :last_pipeline_id
has_many :latest_builds_report_results, through: :latest_builds, source: :report_results
has_many :pipeline_artifacts, class_name: 'Ci::PipelineArtifact', inverse_of: :pipeline, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -119,6 +121,7 @@ module Ci
accepts_nested_attributes_for :variables, reject_if: :persisted?
delegate :full_path, to: :project, prefix: true
+ delegate :title, to: :pipeline_metadata, allow_nil: true
validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
@@ -614,6 +617,15 @@ module Ci
# auto_canceled_by_pipeline_id - store the pipeline_id of the pipeline that triggered cancellation
# execute_async - if true cancel the children asyncronously
def cancel_running(retries: 1, cascade_to_children: true, auto_canceled_by_pipeline_id: nil, execute_async: true)
+ Gitlab::AppJsonLogger.info(
+ event: 'pipeline_cancel_running',
+ pipeline_id: id,
+ auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id,
+ cascade_to_children: cascade_to_children,
+ execute_async: execute_async,
+ **Gitlab::ApplicationContext.current
+ )
+
update(auto_canceled_by_id: auto_canceled_by_pipeline_id) if auto_canceled_by_pipeline_id
cancel_jobs(cancelable_statuses, retries: retries, auto_canceled_by_pipeline_id: auto_canceled_by_pipeline_id)
@@ -760,8 +772,14 @@ module Ci
# There is no ActiveRecord relation between Ci::Pipeline and notes
# as they are related to a commit sha. This method helps importing
# them using the +Gitlab::ImportExport::Project::RelationFactory+ class.
- def notes=(notes)
- notes.each do |note|
+ def notes=(notes_to_save)
+ notes_to_save.reject! do |note_to_save|
+ notes.any? do |note|
+ [note_to_save.note, note_to_save.created_at.to_i] == [note.note, note.created_at.to_i]
+ end
+ end
+
+ notes_to_save.each do |note|
note[:id] = nil
note[:commit_id] = sha
note[:noteable_id] = self['id']
@@ -850,7 +868,6 @@ module Ci
variables.append(key: 'CI_COMMIT_REF_NAME', value: source_ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: source_ref_slug)
variables.append(key: 'CI_COMMIT_BRANCH', value: ref) if branch?
- variables.append(key: 'CI_COMMIT_TAG', value: ref) if tag?
variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
@@ -863,7 +880,8 @@ module Ci
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_BUILD_REF_NAME', value: source_ref)
variables.append(key: 'CI_BUILD_REF_SLUG', value: source_ref_slug)
- variables.append(key: 'CI_BUILD_TAG', value: ref) if tag?
+
+ variables.concat(predefined_commit_tag_variables)
end
end
end
@@ -888,6 +906,20 @@ module Ci
end
end
+ def predefined_commit_tag_variables
+ strong_memoize(:predefined_commit_ref_variables) do
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ next variables unless tag?
+
+ variables.append(key: 'CI_COMMIT_TAG', value: ref)
+ variables.append(key: 'CI_COMMIT_TAG_MESSAGE', value: project.repository.find_tag(ref).message)
+
+ # legacy variable
+ variables.append(key: 'CI_BUILD_TAG', value: ref)
+ end
+ end
+ end
+
def queued_duration
return unless started_at
@@ -972,8 +1004,8 @@ module Ci
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/340781#note_699114700
expanded_environment_names =
builds_in_self_and_project_descendants.joins(:metadata)
- .where.not('ci_builds_metadata.expanded_environment_name' => nil)
- .distinct('ci_builds_metadata.expanded_environment_name')
+ .where.not(Ci::BuildMetadata.table_name => { expanded_environment_name: nil })
+ .distinct("#{Ci::BuildMetadata.quoted_table_name}.expanded_environment_name")
.limit(100)
.pluck(:expanded_environment_name)
@@ -1162,6 +1194,10 @@ module Ci
complete? && builds.latest.with_exposed_artifacts.exists?
end
+ def has_erasable_artifacts?
+ complete? && builds.latest.with_erasable_artifacts.exists?
+ end
+
def branch_updated?
strong_memoize(:branch_updated) do
push_details.branch_updated?
@@ -1328,9 +1364,9 @@ module Ci
self.builds.latest.build_matchers(project)
end
- def authorized_cluster_agents
- strong_memoize(:authorized_cluster_agents) do
- ::Clusters::AgentAuthorizationsFinder.new(project).execute.map(&:agent)
+ def cluster_agent_authorizations
+ strong_memoize(:cluster_agent_authorizations) do
+ ::Clusters::AgentAuthorizationsFinder.new(project).execute
end
end
diff --git a/app/models/ci/pipeline_metadata.rb b/app/models/ci/pipeline_metadata.rb
new file mode 100644
index 00000000000..c96b395b45f
--- /dev/null
+++ b/app/models/ci/pipeline_metadata.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ci
+ class PipelineMetadata < Ci::ApplicationRecord
+ self.primary_key = :pipeline_id
+
+ belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :pipeline_metadata
+ belongs_to :project, class_name: "Project", inverse_of: :pipeline_metadata
+
+ validates :pipeline, presence: true
+ validates :project, presence: true
+ validates :title, presence: true, length: { minimum: 1, maximum: 255 }
+ end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 28d9edcc135..3be627989b1 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -14,7 +14,7 @@ module Ci
include Presentable
include EachBatch
- add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced?
+ add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration
enum access_level: {
not_protected: 0,
@@ -99,27 +99,26 @@ module Ci
}
scope :belonging_to_group, -> (group_id) {
- joins(:runner_namespaces)
- .where(ci_runner_namespaces: { namespace_id: group_id })
+ joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: group_id })
}
scope :belonging_to_group_or_project_descendants, -> (group_id) {
group_ids = Ci::NamespaceMirror.by_group_and_descendants(group_id).select(:namespace_id)
project_ids = Ci::ProjectMirror.by_namespace_id(group_ids).select(:project_id)
- group_runners = joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: group_ids })
- project_runners = joins(:runner_projects).where(ci_runner_projects: { project_id: project_ids })
+ group_runners = belonging_to_group(group_ids)
+ project_runners = belonging_to_project(project_ids).distinct
- union_sql = ::Gitlab::SQL::Union.new([group_runners, project_runners]).to_sql
-
- from("(#{union_sql}) #{table_name}")
+ from_union(
+ [group_runners, project_runners],
+ remove_duplicates: false
+ )
}
scope :belonging_to_group_and_ancestors, -> (group_id) {
group_self_and_ancestors_ids = ::Group.find_by(id: group_id)&.self_and_ancestor_ids
- joins(:runner_namespaces)
- .where(ci_runner_namespaces: { namespace_id: group_self_and_ancestors_ids })
+ belonging_to_group(group_self_and_ancestors_ids)
}
scope :belonging_to_parent_group_of_project, -> (project_id) {
@@ -153,6 +152,17 @@ module Ci
)
end
+ scope :usable_from_scope, -> (group) do
+ from_union(
+ [
+ belonging_to_group(group.ancestor_ids),
+ belonging_to_group_or_project_descendants(group.id),
+ group.shared_runners
+ ],
+ remove_duplicates: false
+ )
+ end
+
scope :assignable_for, ->(project) do
# FIXME: That `to_sql` is needed to workaround a weird Rails bug.
# Without that, placeholders would miss one and couldn't match.
@@ -205,7 +215,7 @@ module Ci
validates :maintenance_note, length: { maximum: 1024 }
- alias_attribute :maintenance_note, :maintainer_note
+ alias_attribute :maintenance_note, :maintainer_note # NOTE: Need to keep until REST v5 is implemented
# Searches for runners matching the given query.
#
@@ -335,7 +345,7 @@ module Ci
end
# DEPRECATED
- # TODO Remove in %16.0 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648
+ # TODO Remove in v5 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648
def deprecated_rest_status
return :stale if stale?
@@ -470,10 +480,6 @@ module Ci
end
end
- def self.token_expiration_enforced?
- Feature.enabled?(:enforce_runner_token_expires_at)
- end
-
private
scope :with_upgrade_status, ->(upgrade_status) do
diff --git a/app/models/ci/secure_file.rb b/app/models/ci/secure_file.rb
index 9a35f1876c9..ffff7eebbee 100644
--- a/app/models/ci/secure_file.rb
+++ b/app/models/ci/secure_file.rb
@@ -7,6 +7,7 @@ module Ci
FILE_SIZE_LIMIT = 5.megabytes.freeze
CHECKSUM_ALGORITHM = 'sha256'
+ PARSABLE_EXTENSIONS = %w[cer p12 mobileprovision].freeze
self.limit_scope = :project
self.limit_name = 'project_ci_secure_files'
@@ -16,6 +17,7 @@ module Ci
validates :file, presence: true, file_size: { maximum: FILE_SIZE_LIMIT }
validates :checksum, :file_store, :name, :project_id, presence: true
validates :name, uniqueness: { scope: :project }
+ validates :metadata, json_schema: { filename: "ci_secure_file_metadata" }, allow_nil: true
after_initialize :generate_key_data
before_validation :assign_checksum
@@ -23,6 +25,8 @@ module Ci
scope :order_by_created_at, -> { order(created_at: :desc) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
+ serialize :metadata, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
+
default_value_for(:file_store) { Ci::SecureFileUploader.default_store }
mount_file_store_uploader Ci::SecureFileUploader
@@ -31,6 +35,41 @@ module Ci
CHECKSUM_ALGORITHM
end
+ def file_extension
+ File.extname(name).delete_prefix('.')
+ end
+
+ def metadata_parsable?
+ PARSABLE_EXTENSIONS.include?(file_extension)
+ end
+
+ def metadata_parser
+ return unless metadata_parsable?
+
+ case file_extension
+ when 'cer'
+ Gitlab::Ci::SecureFiles::Cer.new(file.read)
+ when 'p12'
+ Gitlab::Ci::SecureFiles::P12.new(file.read)
+ when 'mobileprovision'
+ Gitlab::Ci::SecureFiles::MobileProvision.new(file.read)
+ end
+ end
+
+ def update_metadata!
+ return unless metadata_parser
+
+ begin
+ parser = metadata_parser
+ self.metadata = parser.metadata
+ self.expires_at = parser.metadata[:expires_at]
+ save!
+ rescue StandardError => err
+ Gitlab::AppLogger.error("Secure File Parser Failure (#{id}): #{err.message} - #{parser.error}.")
+ nil
+ end
+ end
+
private
def assign_checksum
diff --git a/app/models/clusters/agents/implicit_authorization.rb b/app/models/clusters/agents/implicit_authorization.rb
index 9f7f653ed65..a365ccdc568 100644
--- a/app/models/clusters/agents/implicit_authorization.rb
+++ b/app/models/clusters/agents/implicit_authorization.rb
@@ -16,7 +16,7 @@ module Clusters
end
def config
- nil
+ {}
end
end
end
diff --git a/app/models/concerns/approvable.rb b/app/models/concerns/approvable.rb
index 1566c53217d..55e138d84fb 100644
--- a/app/models/concerns/approvable.rb
+++ b/app/models/concerns/approvable.rb
@@ -50,11 +50,11 @@ module Approvable
approvals.where(user: user).any?
end
- def can_be_approved_by?(user)
+ def eligible_for_approval_by?(user)
user && !approved_by?(user) && user.can?(:approve_merge_request, self)
end
- def can_be_unapproved_by?(user)
+ def eligible_for_unapproval_by?(user)
user && approved_by?(user) && user.can?(:approve_merge_request, self)
end
end
diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
index 88f577c3e23..14be924f9da 100644
--- a/app/models/concerns/atomic_internal_id.rb
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -174,6 +174,13 @@ module AtomicInternalId
#
# bulk_insert(attributes)
# end
+ #
+ # - track_#{scope}_#{column}!
+ # This method can be used to set a new greatest IID value during import operations.
+ #
+ # Example:
+ #
+ # MyClass.track_project_iid!(project, value)
def define_singleton_internal_id_methods(scope, column, init)
define_singleton_method("with_#{scope}_#{column}_supply") do |scope_value, &block|
subject = find_by(scope => scope_value) || self
@@ -183,6 +190,16 @@ module AtomicInternalId
supply = Supply.new(-> { InternalId.generate_next(subject, scope_attrs, usage, init) })
block.call(supply)
end
+
+ define_singleton_method("track_#{scope}_#{column}!") do |scope_value, value|
+ InternalId.track_greatest(
+ self,
+ ::AtomicInternalId.scope_attrs(scope_value),
+ ::AtomicInternalId.scope_usage(self),
+ value,
+ init
+ )
+ end
end
end
diff --git a/app/models/concerns/boards/listable.rb b/app/models/concerns/boards/listable.rb
index b9827a79422..b09ef7e612d 100644
--- a/app/models/concerns/boards/listable.rb
+++ b/app/models/concerns/boards/listable.rb
@@ -13,7 +13,7 @@ module Boards
scope :ordered, -> { order(:list_type, :position) }
scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) }
scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) }
- scope :without_types, ->(list_types) { where.not(list_type: list_types) }
+ scope :with_types, ->(list_types) { where(list_type: list_types) }
class << self
def preload_preferences_for_user(lists, user)
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 9ee0fd1db1d..ec0cf36d875 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -237,3 +237,5 @@ module CacheMarkdownField
end
end
end
+
+CacheMarkdownField.prepend_mod
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 71b26b70bbf..ff884984099 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -80,7 +80,7 @@ module Ci
end
def id_tokens?
- !!metadata&.id_tokens?
+ metadata&.id_tokens.present?
end
def id_tokens=(value)
diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb
index 710ee1ba64f..df803180e77 100644
--- a/app/models/concerns/ci/partitionable.rb
+++ b/app/models/concerns/ci/partitionable.rb
@@ -19,7 +19,32 @@ module Ci
extend ActiveSupport::Concern
include ::Gitlab::Utils::StrongMemoize
+ module Testing
+ InclusionError = Class.new(StandardError)
+
+ PARTITIONABLE_MODELS = %w[
+ CommitStatus
+ Ci::BuildMetadata
+ Ci::Stage
+ Ci::JobArtifact
+ Ci::PipelineVariable
+ Ci::Pipeline
+ ].freeze
+
+ def self.check_inclusion(klass)
+ return if PARTITIONABLE_MODELS.include?(klass.name)
+
+ raise Partitionable::Testing::InclusionError,
+ "#{klass} must be included in PARTITIONABLE_MODELS"
+
+ rescue InclusionError => e
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
+ end
+ end
+
included do
+ Partitionable::Testing.check_inclusion(self)
+
before_validation :set_partition_id, on: :create
validates :partition_id, presence: true
@@ -37,6 +62,8 @@ module Ci
def partitionable(scope:)
define_method(:partition_scope_value) do
strong_memoize(:partition_scope_value) do
+ next Ci::Pipeline.current_partition_value if respond_to?(:importing?) && importing?
+
record = scope.to_proc.call(self)
record.respond_to?(:partition_id) ? record.partition_id : record
end
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index 64d178b7507..03e062a9855 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -95,7 +95,7 @@ module CounterAttribute
next if increment_value == 0
transaction do
- unsafe_update_counters(id, attribute => increment_value)
+ update_counters_with_lease({ attribute => increment_value })
redis_state { |redis| redis.del(flushed_key) }
new_db_value = reset.read_attribute(attribute)
end
@@ -130,9 +130,18 @@ module CounterAttribute
end
end
- def clear_counter!(attribute)
+ def update_counters_with_lease(increments)
+ detect_race_on_record(log_fields: { caller: __method__, attributes: increments.keys }) do
+ self.class.update_counters(id, increments)
+ end
+ end
+
+ def reset_counter!(attribute)
if counter_attribute_enabled?(attribute)
- redis_state { |redis| redis.del(counter_key(attribute)) }
+ detect_race_on_record(log_fields: { caller: __method__, attributes: attribute }) do
+ update!(attribute => 0)
+ clear_counter!(attribute)
+ end
log_clear_counter(attribute)
end
@@ -164,14 +173,20 @@ module CounterAttribute
private
+ def database_lock_key
+ "project:{#{project_id}}:#{self.class}:#{id}"
+ end
+
def steal_increments(increment_key, flushed_key)
redis_state do |redis|
redis.eval(LUA_STEAL_INCREMENT_SCRIPT, keys: [increment_key, flushed_key])
end
end
- def unsafe_update_counters(id, increments)
- self.class.update_counters(id, increments)
+ def clear_counter!(attribute)
+ redis_state do |redis|
+ redis.del(counter_key(attribute))
+ end
end
def execute_after_flush_callbacks
@@ -192,6 +207,44 @@ module CounterAttribute
# a worker is already updating the counters
end
+ # detect_race_on_record uses a lease to monitor access
+ # to the project statistics row. This is needed to detect
+ # concurrent attempts to increment columns, which could result in a
+ # race condition.
+ #
+ # As the purpose is to detect and warn concurrent attempts,
+ # it falls back to direct update on the row if it fails to obtain the lease.
+ #
+ # It does not guarantee that there will not be any concurrent updates.
+ def detect_race_on_record(log_fields: {})
+ return yield unless Feature.enabled?(:counter_attribute_db_lease_for_update, project)
+
+ # Ensure attributes is always an array before we log
+ log_fields[:attributes] = Array(log_fields[:attributes])
+
+ Gitlab::AppLogger.info(
+ message: 'Acquiring lease for project statistics update',
+ project_statistics_id: id,
+ project_id: project.id,
+ **log_fields,
+ **Gitlab::ApplicationContext.current
+ )
+
+ in_lock(database_lock_key, retries: 0) do
+ yield
+ end
+ rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
+ Gitlab::AppLogger.warn(
+ message: 'Concurrent project statistics update detected',
+ project_statistics_id: id,
+ project_id: project.id,
+ **log_fields,
+ **Gitlab::ApplicationContext.current
+ )
+
+ yield
+ end
+
def log_increment_counter(attribute, increment, new_value)
payload = Gitlab::ApplicationContext.current.merge(
message: 'Increment counter attribute',
diff --git a/app/models/concerns/has_wiki.rb b/app/models/concerns/has_wiki.rb
index 89bcabafb84..53016ce62f4 100644
--- a/app/models/concerns/has_wiki.rb
+++ b/app/models/concerns/has_wiki.rb
@@ -8,7 +8,7 @@ module HasWiki
end
def create_wiki
- wiki.wiki
+ wiki.create_wiki_repository
true
rescue Wiki::CouldNotCreateWikiError
errors.add(:base, _('Failed to create wiki'))
diff --git a/app/models/concerns/integrations/base_data_fields.rb b/app/models/concerns/integrations/base_data_fields.rb
index 2870922d90d..4319d63abb9 100644
--- a/app/models/concerns/integrations/base_data_fields.rb
+++ b/app/models/concerns/integrations/base_data_fields.rb
@@ -5,8 +5,6 @@ module Integrations
extend ActiveSupport::Concern
included do
- # TODO: Once we rename the tables we can't rely on `table_name` anymore.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/331953
belongs_to :integration, inverse_of: self.table_name.to_sym, foreign_key: :integration_id
validates :integration, presence: true
diff --git a/app/models/concerns/integrations/has_web_hook.rb b/app/models/concerns/integrations/has_web_hook.rb
index 5fd71f3d72f..e622faf4a51 100644
--- a/app/models/concerns/integrations/has_web_hook.rb
+++ b/app/models/concerns/integrations/has_web_hook.rb
@@ -6,7 +6,7 @@ module Integrations
included do
after_save :update_web_hook!, if: :activated?
- has_one :service_hook, inverse_of: :integration, foreign_key: :service_id
+ has_one :service_hook, inverse_of: :integration, foreign_key: :integration_id
end
# Return the URL to be used for the webhook.
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index b81a9b51e1c..f8389865f91 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -33,6 +33,7 @@ module Issuable
DESCRIPTION_LENGTH_MAX = 1.megabyte
DESCRIPTION_HTML_LENGTH_MAX = 5.megabytes
SEARCHABLE_FIELDS = %w(title description).freeze
+ MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS = 200
STATE_ID_MAP = {
opened: 1,
@@ -95,6 +96,7 @@ module Issuable
# to avoid breaking the existing Issuables which may have their descriptions longer
validates :description, length: { maximum: DESCRIPTION_LENGTH_MAX }, allow_blank: true, on: :create
validate :description_max_length_for_new_records_is_valid, on: :update
+ validate :validate_assignee_size_length, unless: :importing?
before_validation :truncate_description_on_import!
@@ -166,6 +168,11 @@ module Issuable
def locking_enabled?
false
end
+
+ def max_number_of_assignees_or_reviewers_message
+ # Assignees will be included in https://gitlab.com/gitlab-org/gitlab/-/issues/368936
+ format(_("total must be less than or equal to %{size}"), size: MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS)
+ end
end
# We want to use optimistic lock for cases when only title or description are involved
@@ -227,11 +234,19 @@ module Issuable
def truncate_description_on_import!
self.description = description&.slice(0, Issuable::DESCRIPTION_LENGTH_MAX) if importing?
end
+
+ def validate_assignee_size_length
+ return true unless Feature.enabled?(:limit_assignees_per_issuable)
+ return true unless assignees.size > MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
+
+ errors.add :assignees,
+ -> (_object, _data) { self.class.max_number_of_assignees_or_reviewers_message }
+ end
end
class_methods do
def participant_includes
- [:assignees, :author, { notes: [:author, :award_emoji] }]
+ [:author, :award_emoji, { notes: [:author, :award_emoji, :system_note_metadata] }]
end
# Searches for records with a matching title.
@@ -383,10 +398,12 @@ module Issuable
milestone_table = Milestone.arel_table
grouping_columns << milestone_table[:id]
grouping_columns << milestone_table[:due_date]
- elsif %w(merged_at_desc merged_at_asc).include?(sort)
+ elsif %w(merged_at_desc merged_at_asc merged_at).include?(sort)
+ grouping_columns << MergeRequest::Metrics.arel_table[:id]
grouping_columns << MergeRequest::Metrics.arel_table[:merged_at]
- elsif %w(closed_at_desc closed_at_asc).include?(sort)
- grouping_columns << MergeRequest::Metrics.arel_table[:closed_at]
+ elsif %w(closed_at_desc closed_at_asc closed_at).include?(sort)
+ grouping_columns << MergeRequest::Metrics.arel_table[:id]
+ grouping_columns << MergeRequest::Metrics.arel_table[:latest_closed_at]
end
grouping_columns
@@ -431,7 +448,16 @@ module Issuable
end
def assignee_or_author?(user)
- author_id == user.id || assignees.exists?(user.id)
+ author_id == user.id || assignee?(user)
+ end
+
+ def assignee?(user)
+ # Necessary so we can preload the association and avoid N + 1 queries
+ if assignees.loaded?
+ assignees.to_a.include?(user)
+ else
+ assignees.exists?(user.id)
+ end
end
def today?
@@ -630,6 +656,14 @@ module Issuable
def draftless_title_changed(old_title)
old_title != title
end
+
+ def read_ability_for(participable_source)
+ return super if participable_source == self
+
+ name = participable_source.try(:issuable_ability_name) || :read_issuable_participables
+
+ { name: name, subject: self }
+ end
end
Issuable.prepend_mod_with('Issuable')
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index 8130adf05f1..6035cb87c9b 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -152,7 +152,9 @@ module Participable
end
def source_visible_to_user?(source, user)
- Ability.allowed?(user, "read_#{source.model_name.element}".to_sym, source)
+ ability = read_ability_for(source)
+
+ Ability.allowed?(user, ability[:name], ability[:subject])
end
def filter_by_ability(participants)
@@ -172,6 +174,14 @@ module Participable
participant.can?(:read_project, project)
end
end
+
+ # Returns Hash containing ability name and subject needed to read a specific participable.
+ # Should be overridden if a different ability is required.
+ def read_ability_for(participable_source)
+ name = participable_source.try(:to_ability_name) || participable_source.model_name.element
+
+ { name: "read_#{name}".to_sym, subject: participable_source }
+ end
end
Participable.prepend_mod_with('Participable')
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 5b759dedb26..262839a3fa6 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -17,6 +17,9 @@ module Routable
def self.find_by_full_path(path, follow_redirects: false, route_scope: Route, redirect_route_scope: RedirectRoute)
return unless path.present?
+ # Convert path to string to prevent DB error: function lower(integer) does not exist
+ path = path.to_s
+
# Case sensitive match first (it's cheaper and the usual case)
# If we didn't have an exact match, we perform a case insensitive search
#
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
index d53594eb5af..5b74e88429c 100644
--- a/app/models/concerns/timebox.rb
+++ b/app/models/concerns/timebox.rb
@@ -3,13 +3,10 @@
module Timebox
extend ActiveSupport::Concern
- include AtomicInternalId
include CacheMarkdownField
include Gitlab::SQL::Pattern
- include IidRoutes
include Referable
include StripAttribute
- include FromUnion
TimeboxStruct = Struct.new(:title, :name, :id, :class_name) do
# Ensure these models match the interface required for exporting
@@ -42,39 +39,19 @@ module Timebox
alias_method :timebox_id, :id
- validates :group, presence: true, unless: :project
- validates :project, presence: true, unless: :group
-
- validate :timebox_type_check
validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
validate :dates_within_4_digits
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_reference_expansion_enabled: true
- belongs_to :project
- belongs_to :group
-
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
has_many :merge_requests
- scope :of_projects, ->(ids) { where(project_id: ids) }
- scope :of_groups, ->(ids) { where(group_id: ids) }
scope :closed, -> { with_state(:closed) }
- scope :for_projects, -> { where(group: nil).includes(:project) }
scope :with_title, -> (title) { where(title: title) }
- scope :for_projects_and_groups, -> (projects, groups) do
- projects = projects.compact if projects.is_a? Array
- projects = [] if projects.nil?
-
- groups = groups.compact if groups.is_a? Array
- groups = [] if groups.nil?
-
- from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false)
- end
-
# A timebox is within the timeframe (start_date, end_date) if it overlaps
# with that timeframe:
#
@@ -132,10 +109,6 @@ module Timebox
end
end
- def count_by_state
- reorder(nil).group(:state).count
- end
-
def predefined_id?(id)
[Any.id, None.id, Upcoming.id, Started.id].include?(id)
end
@@ -145,29 +118,8 @@ module Timebox
end
end
- ##
- # Returns the String necessary to reference a Timebox in Markdown. Group
- # timeboxes only support name references, and do not support cross-project
- # references.
- #
- # format - Symbol format to use (default: :iid, optional: :name)
- #
- # Examples:
- #
- # Milestone.first.to_reference # => "%1"
- # Iteration.first.to_reference(format: :name) # => "*iteration:\"goal\""
- # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-foss%1"
- # Iteration.first.to_reference(same_namespace_project) # => "gitlab-foss*iteration:1"
- #
- def to_reference(from = nil, format: :name, full: false)
- format_reference = timebox_format_reference(format)
- reference = "#{self.class.reference_prefix}#{format_reference}"
-
- if project
- "#{project.to_reference_base(from, full: full)}#{reference}"
- else
- reference
- end
+ def to_reference
+ raise NotImplementedError
end
def reference_link_text(from = nil)
@@ -182,20 +134,12 @@ module Timebox
model_name.singular
end
- def group_timebox?
- group_id.present?
- end
-
- def project_timebox?
- project_id.present?
- end
-
def safe_title
title.to_slug.normalize.to_s
end
def resource_parent
- group || project
+ raise NotImplementedError
end
def to_ability_name
@@ -203,13 +147,7 @@ module Timebox
end
def merge_requests_enabled?
- if group_timebox?
- # Assume that groups have at least one project with merge requests enabled.
- # Otherwise, we would need to load all of the projects from the database.
- true
- elsif project_timebox?
- project&.merge_requests_enabled?
- end
+ raise NotImplementedError
end
def weight_available?
@@ -218,28 +156,6 @@ module Timebox
private
- def timebox_format_reference(format = :iid)
- raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format)
-
- if group_timebox? && format == :iid
- raise ArgumentError, _('Cannot refer to a group %{timebox_type} by an internal id!') % { timebox_type: timebox_name }
- end
-
- if format == :name && !name.include?('"')
- %("#{name}")
- else
- iid
- end
- end
-
- # Timebox should be either a project timebox or a group timebox
- def timebox_type_check
- if group_id && project_id
- field = project_id_changed? ? :project_id : :group_id
- errors.add(field, _("%{timebox_name} should belong either to a project or a group.") % { timebox_name: timebox_name })
- end
- end
-
def start_date_should_be_less_than_due_date
if due_date <= start_date
errors.add(:due_date, _("must be greater than start date"))
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 94ac2405f61..2563fd484f1 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -4,6 +4,7 @@ class DeployKey < Key
include FromUnion
include IgnorableColumns
include PolicyActor
+ include Presentable
has_many :deploy_keys_projects, inverse_of: :deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :deploy_keys_projects
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index dafcbc593be..20841bc14cd 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -105,6 +105,7 @@ class Deployment < ApplicationRecord
after_transition any => :running do |deployment|
next unless deployment.project.ci_forward_deployment_enabled?
+ next if Feature.enabled?(:prevent_outdated_deployment_jobs, deployment.project)
deployment.run_after_commit do
Deployments::DropOlderDeploymentsWorker.perform_async(id)
@@ -282,27 +283,11 @@ class Deployment < ApplicationRecord
end
def manual_actions
- environment_manual_actions
- end
-
- def other_manual_actions
- @other_manual_actions ||= deployable.try(:other_manual_actions)
- end
-
- def environment_manual_actions
- @environment_manual_actions ||= deployable.try(:environment_manual_actions)
+ @manual_actions ||= deployable.try(:other_manual_actions)
end
def scheduled_actions
- environment_scheduled_actions
- end
-
- def environment_scheduled_actions
- @environment_scheduled_actions ||= deployable.try(:environment_scheduled_actions)
- end
-
- def other_scheduled_actions
- @other_scheduled_actions ||= deployable.try(:other_scheduled_actions)
+ @scheduled_actions ||= deployable.try(:other_scheduled_actions)
end
def playable_build
diff --git a/app/models/diff_viewer/server_side.rb b/app/models/diff_viewer/server_side.rb
index 0877c9dddec..a1defb2594f 100644
--- a/app/models/diff_viewer/server_side.rb
+++ b/app/models/diff_viewer/server_side.rb
@@ -10,6 +10,9 @@ module DiffViewer
end
def prepare!
+ return if Feature.enabled?(:disable_load_entire_blob_for_diff_viewer, diff_file.repository.project)
+
+ # TODO: remove this after resolving #342703
diff_file.old_blob&.load_all_data!
diff_file.new_blob&.load_all_data!
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 4b98cd02e3b..2d3f342953f 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -71,7 +71,7 @@ class Environment < ApplicationRecord
validate :safe_external_url
validate :merge_request_not_changed
- delegate :manual_actions, :other_manual_actions, to: :last_deployment, allow_nil: true
+ delegate :manual_actions, to: :last_deployment, allow_nil: true
delegate :auto_rollback_enabled?, to: :project
scope :available, -> { with_state(:available) }
@@ -332,9 +332,9 @@ class Environment < ApplicationRecord
end
def actions_for(environment)
- return [] unless other_manual_actions
+ return [] unless manual_actions
- other_manual_actions.select do |action|
+ manual_actions.select do |action|
action.expanded_environment_name == environment
end
end
@@ -441,11 +441,15 @@ class Environment < ApplicationRecord
end
def auto_stop_in=(value)
- return unless value
+ if value.nil?
+ # Handles edge case when auto_stop_at is already set and the new value is nil.
+ # Possible by setting `auto_stop_in: null` in the CI configuration yml.
+ self.auto_stop_at = nil
- parser = ::Gitlab::Ci::Build::DurationParser.new(value)
+ return
+ end
- return if parser.seconds_from_now.nil? && auto_stop_at.nil?
+ parser = ::Gitlab::Ci::Build::DurationParser.new(value)
self.auto_stop_at = parser.seconds_from_now
rescue ChronicDuration::DurationParseError => ex
@@ -540,7 +544,7 @@ class Environment < ApplicationRecord
self.class.tiers[:development]
when /(test|tst|int|ac(ce|)pt|qa|qc|control|quality)/i
self.class.tiers[:testing]
- when /(st(a|)g|mod(e|)l|pre|demo)/i
+ when /(st(a|)g|mod(e|)l|pre|demo|non)/i
self.class.tiers[:staging]
when /(pr(o|)d|live)/i
self.class.tiers[:production]
diff --git a/app/models/event.rb b/app/models/event.rb
index a20ca0dc423..4c1793d3f13 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -10,9 +10,6 @@ class Event < ApplicationRecord
include UsageStatistics
include ShaAttribute
- # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/358088
- default_scope { reorder(nil) } # rubocop:disable Cop/DefaultScope
-
ACTIONS = HashWithIndifferentAccess.new(
created: 1,
updated: 2,
@@ -281,6 +278,7 @@ class Event < ApplicationRecord
"opened"
end
end
+
# rubocop: enable Metrics/CyclomaticComplexity
# rubocop: enable Metrics/PerceivedComplexity
@@ -448,9 +446,9 @@ class Event < ApplicationRecord
def design_action_names
{
- created: _('added'),
- updated: _('updated'),
- destroyed: _('removed')
+ created: 'added',
+ updated: 'updated',
+ destroyed: 'removed'
}
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 1445e71b0bc..38623d91705 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -904,11 +904,7 @@ class Group < Namespace
end
def packages_policy_subject
- if Feature.enabled?(:read_package_policy_rule, self)
- ::Packages::Policies::Group.new(self)
- else
- self
- end
+ ::Packages::Policies::Group.new(self)
end
def update_two_factor_requirement_for_members
diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb
index 7005c8593bd..15949570f9c 100644
--- a/app/models/group_group_link.rb
+++ b/app/models/group_group_link.rb
@@ -8,7 +8,7 @@ class GroupGroupLink < ApplicationRecord
validates :shared_group, presence: true
validates :shared_group_id, uniqueness: { scope: [:shared_with_group_id],
- message: _('The group has already been shared with this group') }
+ message: N_('The group has already been shared with this group') }
validates :shared_with_group, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.all_values },
presence: true
diff --git a/app/models/group_label.rb b/app/models/group_label.rb
index ff14529c6e6..0d2eb524929 100644
--- a/app/models/group_label.rb
+++ b/app/models/group_label.rb
@@ -2,6 +2,7 @@
class GroupLabel < Label
belongs_to :group
+ belongs_to :parent_container, foreign_key: :group_id, class_name: 'Group'
validates :group, presence: true
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index bcbf43ee38b..dcba136d163 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -55,13 +55,6 @@ class ProjectHook < WebHook
redis.set(key, time) if !prev || prev < time
end
end
-
- private
-
- override :web_hooks_disable_failed?
- def web_hooks_disable_failed?
- Feature.enabled?(:web_hooks_disable_failed, project)
- end
end
ProjectHook.prepend_mod_with('ProjectHook')
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 80e167b350b..27119d3a95a 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -4,7 +4,7 @@ class ServiceHook < WebHook
include Presentable
extend ::Gitlab::Utils::Override
- belongs_to :integration, foreign_key: :service_id
+ belongs_to :integration
validates :integration, presence: true
def execute(data, hook_name = 'service_hook')
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 84ee23d77ce..71794964c99 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -7,7 +7,7 @@ class WebHook < ApplicationRecord
MAX_FAILURES = 100
FAILURE_THRESHOLD = 3 # three strikes
- INITIAL_BACKOFF = 10.minutes
+ INITIAL_BACKOFF = 1.minute
MAX_BACKOFF = 1.day
BACKOFF_GROWTH_FACTOR = 2.0
@@ -53,18 +53,24 @@ class WebHook < ApplicationRecord
where('recent_failures > ? OR disabled_until >= ?', FAILURE_THRESHOLD, Time.current)
end
+ def self.web_hooks_disable_failed?(hook)
+ Feature.enabled?(:web_hooks_disable_failed, hook.parent)
+ end
+
def executable?
!temporarily_disabled? && !permanently_disabled?
end
def temporarily_disabled?
return false unless web_hooks_disable_failed?
+ return false if recent_failures <= FAILURE_THRESHOLD
disabled_until.present? && disabled_until >= Time.current
end
def permanently_disabled?
return false unless web_hooks_disable_failed?
+ return false if disabled_until.present?
recent_failures > FAILURE_THRESHOLD
end
@@ -112,17 +118,26 @@ class WebHook < ApplicationRecord
save(validate: false)
end
+ # Don't actually back-off until FAILURE_THRESHOLD failures have been seen
+ # we mark the grace-period using the recent_failures counter
def backoff!
return if permanently_disabled? || (backoff_count >= MAX_FAILURES && temporarily_disabled?)
- assign_attributes(disabled_until: next_backoff.from_now, backoff_count: backoff_count.succ.clamp(0, MAX_FAILURES))
+ attrs = { recent_failures: recent_failures + 1 }
+
+ if recent_failures >= FAILURE_THRESHOLD
+ attrs[:backoff_count] = backoff_count.succ.clamp(1, MAX_FAILURES)
+ attrs[:disabled_until] = next_backoff.from_now
+ end
+
+ assign_attributes(attrs)
save(validate: false)
end
def failed!
return unless recent_failures < MAX_FAILURES
- assign_attributes(recent_failures: recent_failures + 1)
+ assign_attributes(disabled_until: nil, backoff_count: 0, recent_failures: recent_failures + 1)
save(validate: false)
end
@@ -186,7 +201,7 @@ class WebHook < ApplicationRecord
private
def web_hooks_disable_failed?
- Feature.enabled?(:web_hooks_disable_failed)
+ self.class.web_hooks_disable_failed?(self)
end
def initialize_url_variables
diff --git a/app/models/incident_management/timeline_event.rb b/app/models/incident_management/timeline_event.rb
index dd0d3c6585d..735d4e4298c 100644
--- a/app/models/incident_management/timeline_event.rb
+++ b/app/models/incident_management/timeline_event.rb
@@ -18,7 +18,13 @@ module IncidentManagement
validates :project, :incident, :occurred_at, presence: true
validates :action, presence: true, length: { maximum: 128 }
- validates :note, :note_html, presence: true, length: { maximum: 10_000 }
+ validates :note, presence: true, length: { maximum: 10_000 }
+ validates :note_html, length: { maximum: 10_000 }
+
+ has_many :timeline_event_tag_links, class_name: 'IncidentManagement::TimelineEventTagLink'
+ has_many :timeline_event_tags,
+ class_name: 'IncidentManagement::TimelineEventTag',
+ through: :timeline_event_tag_links
scope :order_occurred_at_asc_id_asc, -> { reorder(occurred_at: :asc, id: :asc) }
end
diff --git a/app/models/incident_management/timeline_event_tag.rb b/app/models/incident_management/timeline_event_tag.rb
new file mode 100644
index 00000000000..cde3afcaa16
--- /dev/null
+++ b/app/models/incident_management/timeline_event_tag.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class TimelineEventTag < ApplicationRecord
+ self.table_name = 'incident_management_timeline_event_tags'
+
+ belongs_to :project, inverse_of: :incident_management_timeline_event_tags
+
+ has_many :timeline_event_tag_links,
+ class_name: 'IncidentManagement::TimelineEventTagLink'
+
+ has_many :timeline_events,
+ class_name: 'IncidentManagement::TimelineEvent',
+ through: :timeline_event_tag_links
+
+ validates :name, presence: true, format: { with: /\A[^,]+\z/ }
+ validates :name, uniqueness: { scope: :project_id }
+ validates :name, length: { maximum: 255 }
+ end
+end
diff --git a/app/models/incident_management/timeline_event_tag_link.rb b/app/models/incident_management/timeline_event_tag_link.rb
new file mode 100644
index 00000000000..912339717a8
--- /dev/null
+++ b/app/models/incident_management/timeline_event_tag_link.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ class TimelineEventTagLink < ApplicationRecord
+ self.table_name = 'incident_management_timeline_event_tag_links'
+
+ belongs_to :timeline_event_tag, class_name: 'IncidentManagement::TimelineEventTag'
+
+ belongs_to :timeline_event, class_name: 'IncidentManagement::TimelineEvent'
+ end
+end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index aecf9529a14..23688a87cbd 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -147,6 +147,8 @@ class Integration < ApplicationRecord
fields << ::Integrations::Field.new(name: name, integration_class: self, **attrs)
case storage
+ when :attribute
+ # noop
when :properties
prop_accessor(name)
when :data_fields
@@ -155,7 +157,7 @@ class Integration < ApplicationRecord
raise ArgumentError, "Unknown field storage: #{storage}"
end
- boolean_accessor(name) if attrs[:type] == 'checkbox'
+ boolean_accessor(name) if attrs[:type] == 'checkbox' && storage != :attribute
end
# :nocov:
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index c9407aa738e..ab0fdbd777f 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -15,7 +15,77 @@ module Integrations
TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x.freeze
- prop_accessor :datadog_site, :api_url, :api_key, :datadog_service, :datadog_env, :datadog_tags
+ field :datadog_site,
+ placeholder: DEFAULT_DOMAIN,
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|The Datadog site to send data to. To send data to the EU site, use %{codeOpen}datadoghq.eu%{codeClose}.')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe
+ }
+ end
+
+ field :api_url,
+ exposes_secrets: true,
+ title: -> { s_('DatadogIntegration|API URL') },
+ help: -> { s_('DatadogIntegration|(Advanced) The full URL for your Datadog site.') }
+
+ field :api_key,
+ type: 'password',
+ title: -> { _('API key') },
+ non_empty_password_title: -> { s_('ProjectService|Enter new API key') },
+ non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current API key') },
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.')
+ ) % {
+ linkOpen: %Q{<a href="#{URL_API_KEYS_DOCS}" target="_blank" rel="noopener noreferrer">}.html_safe,
+ linkClose: '</a>'.html_safe
+ }
+ end,
+ required: true
+
+ field :archive_trace_events,
+ storage: :attribute,
+ type: 'checkbox',
+ title: -> { s_('Logs') },
+ checkbox_label: -> { s_('Enable logs collection') },
+ help: -> { s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.') }
+
+ field :datadog_service,
+ title: -> { s_('DatadogIntegration|Service') },
+ placeholder: 'gitlab-ci',
+ help: -> { s_('DatadogIntegration|Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments.') }
+
+ field :datadog_env,
+ title: -> { s_('DatadogIntegration|Environment') },
+ placeholder: 'ci',
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|For self-managed deployments, set the %{codeOpen}env%{codeClose} tag for all the data sent to Datadog. %{linkOpen}How do I use tags?%{linkClose}')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe,
+ linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
+ linkClose: '</a>'.html_safe
+ }
+ end
+
+ field :datadog_tags,
+ type: 'textarea',
+ title: -> { s_('DatadogIntegration|Tags') },
+ placeholder: "tag:value\nanother_tag:value",
+ help: -> do
+ ERB::Util.html_escape(
+ s_('DatadogIntegration|Custom tags in Datadog. Enter one tag per line in the %{codeOpen}key:value%{codeClose} format. %{linkOpen}How do I use tags?%{linkClose}')
+ ) % {
+ codeOpen: '<code>'.html_safe,
+ codeClose: '</code>'.html_safe,
+ linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
+ linkClose: '</a>'.html_safe
+ }
+ end
before_validation :strip_properties
@@ -68,87 +138,6 @@ module Integrations
'datadog'
end
- def fields
- [
- {
- type: 'text',
- name: 'datadog_site',
- placeholder: DEFAULT_DOMAIN,
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|The Datadog site to send data to. To send data to the EU site, use %{codeOpen}datadoghq.eu%{codeClose}.')
- ) % {
- codeOpen: '<code>'.html_safe,
- codeClose: '</code>'.html_safe
- },
- required: false
- },
- {
- type: 'text',
- name: 'api_url',
- title: s_('DatadogIntegration|API URL'),
- help: s_('DatadogIntegration|(Advanced) The full URL for your Datadog site.'),
- required: false
- },
- {
- type: 'password',
- name: 'api_key',
- title: _('API key'),
- non_empty_password_title: s_('ProjectService|Enter new API key'),
- non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'),
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|%{linkOpen}API key%{linkClose} used for authentication with Datadog.')
- ) % {
- linkOpen: %Q{<a href="#{URL_API_KEYS_DOCS}" target="_blank" rel="noopener noreferrer">}.html_safe,
- linkClose: '</a>'.html_safe
- },
- required: true
- },
- {
- type: 'checkbox',
- name: 'archive_trace_events',
- title: s_('Logs'),
- checkbox_label: s_('Enable logs collection'),
- help: s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.'),
- required: false
- },
- {
- type: 'text',
- name: 'datadog_service',
- title: s_('DatadogIntegration|Service'),
- placeholder: 'gitlab-ci',
- help: s_('DatadogIntegration|Tag all data from this GitLab instance in Datadog. Useful when managing several self-managed deployments.')
- },
- {
- type: 'text',
- name: 'datadog_env',
- title: s_('DatadogIntegration|Environment'),
- placeholder: 'ci',
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|For self-managed deployments, set the %{codeOpen}env%{codeClose} tag for all the data sent to Datadog. %{linkOpen}How do I use tags?%{linkClose}')
- ) % {
- codeOpen: '<code>'.html_safe,
- codeClose: '</code>'.html_safe,
- linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
- linkClose: '</a>'.html_safe
- }
- },
- {
- type: 'textarea',
- name: 'datadog_tags',
- title: s_('DatadogIntegration|Tags'),
- placeholder: "tag:value\nanother_tag:value",
- help: ERB::Util.html_escape(
- s_('DatadogIntegration|Custom tags in Datadog. Enter one tag per line in the %{codeOpen}key:value%{codeClose} format. %{linkOpen}How do I use tags?%{linkClose}')
- ) % {
- codeOpen: '<code>'.html_safe,
- codeClose: '</code>'.html_safe,
- linkOpen: '<a href="https://docs.datadoghq.com/getting_started/tagging/#using-tags" target="_blank" rel="noopener noreferrer">'.html_safe,
- linkClose: '</a>'.html_safe
- }
- }
- ]
- end
-
override :hook_url
def hook_url
url = api_url.presence || sprintf(URL_TEMPLATE, datadog_domain: datadog_domain)
diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb
index 58eabcfd378..01a04743d5d 100644
--- a/app/models/integrations/harbor.rb
+++ b/app/models/integrations/harbor.rb
@@ -3,14 +3,33 @@ require 'uri'
module Integrations
class Harbor < Integration
- prop_accessor :url, :project_name, :username, :password
-
validates :url, public_url: true, presence: true, addressable_url: { allow_localhost: false, allow_local_network: false }, if: :activated?
validates :project_name, presence: true, if: :activated?
validates :username, presence: true, if: :activated?
validates :password, format: { with: ::Ci::Maskable::REGEX }, if: :activated?
- before_validation :reset_username_and_password
+ field :url,
+ title: -> { s_('HarborIntegration|Harbor URL') },
+ placeholder: 'https://demo.goharbor.io',
+ help: -> { s_('HarborIntegration|Base URL of the Harbor instance.') },
+ exposes_secrets: true,
+ required: true
+
+ field :project_name,
+ title: -> { s_('HarborIntegration|Harbor project name') },
+ help: -> { s_('HarborIntegration|The name of the project in Harbor.') }
+
+ field :username,
+ title: -> { s_('HarborIntegration|Harbor username') },
+ required: true
+
+ field :password,
+ type: 'password',
+ title: -> { s_('HarborIntegration|Harbor password') },
+ help: -> { s_('HarborIntegration|Password for your Harbor username.') },
+ non_empty_password_title: -> { s_('HarborIntegration|Enter new Harbor password') },
+ non_empty_password_help: -> { s_('HarborIntegration|Leave blank to use your current password.') },
+ required: true
def title
'Harbor'
@@ -21,7 +40,7 @@ module Integrations
end
def help
- s_("HarborIntegration|After the Harbor integration is activated, global variables '$HARBOR_USERNAME', '$HARBOR_HOST', '$HARBOR_OCI', '$HARBOR_PASSWORD', '$HARBOR_URL' and '$HARBOR_PROJECT' will be created for CI/CD use.")
+ s_("HarborIntegration|After the Harbor integration is activated, global variables `$HARBOR_USERNAME`, `$HARBOR_HOST`, `$HARBOR_OCI`, `$HARBOR_PASSWORD`, `$HARBOR_URL` and `$HARBOR_PROJECT` will be created for CI/CD use.")
end
def hostname
@@ -46,40 +65,6 @@ module Integrations
client.ping
end
- def fields
- [
- {
- type: 'text',
- name: 'url',
- title: s_('HarborIntegration|Harbor URL'),
- placeholder: 'https://demo.goharbor.io',
- help: s_('HarborIntegration|Base URL of the Harbor instance.'),
- required: true
- },
- {
- type: 'text',
- name: 'project_name',
- title: s_('HarborIntegration|Harbor project name'),
- help: s_('HarborIntegration|The name of the project in Harbor.')
- },
- {
- type: 'text',
- name: 'username',
- title: s_('HarborIntegration|Harbor username'),
- required: true
- },
- {
- type: 'password',
- name: 'password',
- title: s_('HarborIntegration|Harbor password'),
- help: s_('HarborIntegration|Password for your Harbor username.'),
- non_empty_password_title: s_('HarborIntegration|Enter new Harbor password'),
- non_empty_password_help: s_('HarborIntegration|Leave blank to use your current password.'),
- required: true
- }
- ]
- end
-
def ci_variables
return [] unless activated?
@@ -100,15 +85,5 @@ module Integrations
def client
@client ||= ::Gitlab::Harbor::Client.new(self)
end
-
- def reset_username_and_password
- if url_changed? && !password_touched?
- self.password = nil
- end
-
- if url_changed? && !username_touched?
- self.username = nil
- end
- end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 754591b8017..ea7acf9a5d1 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -40,10 +40,15 @@ class Issue < ApplicationRecord
SORTING_PREFERENCE_FIELD = :issues_sort
- # Types of issues that should be displayed on lists across the app
- # for example, project issues list, group issues list and issue boards.
- # Some issue types, like test cases, should be hidden by default.
- TYPES_FOR_LIST = %w(issue incident).freeze
+ # Types of issues that should be displayed on issue lists across the app
+ # for example, project issues list, group issues list, and issues dashboard.
+ #
+ # This should be kept consistent with the enums used for the GraphQL issue list query in
+ # https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/assets/javascripts/issues/list/constants.js#L154-158
+ TYPES_FOR_LIST = %w(issue incident test_case task).freeze
+
+ # Types of issues that should be displayed on issue board lists
+ TYPES_FOR_BOARD_LIST = %w(issue incident).freeze
belongs_to :project
belongs_to :namespace, inverse_of: :issues
@@ -107,6 +112,7 @@ class Issue < ApplicationRecord
enum issue_type: WorkItems::Type.base_types
alias_method :issuing_parent, :project
+ alias_attribute :issuing_parent_id, :project_id
alias_attribute :external_author, :service_desk_reply_to
@@ -270,6 +276,10 @@ class Issue < ApplicationRecord
end
end
+ def self.participant_includes
+ [:assignees] + super
+ end
+
def next_object_by_relative_position(ignoring: nil, order: :asc)
array_mapping_scope = -> (id_expression) do
relation = Issue.where(Issue.arel_table[:project_id].eq(id_expression))
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index 71ecbcf1c1a..ed73793c78f 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -2,6 +2,12 @@
# Placeholder class for model that is implemented in EE
class Iteration < ApplicationRecord
+ include IgnorableColumns
+
+ # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/372125
+ # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/372126
+ ignore_column :project_id, remove_with: '15.6', remove_after: '2022-09-17'
+
self.table_name = 'sprints'
def self.reference_prefix
diff --git a/app/models/jira_connect/public_key.rb b/app/models/jira_connect/public_key.rb
new file mode 100644
index 00000000000..8959884861b
--- /dev/null
+++ b/app/models/jira_connect/public_key.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module JiraConnect
+ class PublicKey
+ # Public keys are created with JWT tokens via JiraConnect::CreateAsymmetricJwtService
+ # They need to be available for third party applications to verify the token.
+ # This should happen right after the application received the token so public keys
+ # only need to exist for a few minutes.
+ REDIS_EXPIRY_TIME = 5.minutes.to_i.freeze
+
+ attr_reader :key, :uuid
+
+ def self.create!(key:)
+ new(key: key, uuid: Gitlab::UUID.v5(SecureRandom.hex)).save!
+ end
+
+ def self.find(uuid)
+ Gitlab::Redis::SharedState.with do |redis|
+ key = redis.get(redis_key(uuid))
+
+ raise ActiveRecord::RecordNotFound if key.nil?
+
+ new(key: key, uuid: uuid)
+ end
+ end
+
+ def initialize(key:, uuid:)
+ key = OpenSSL::PKey.read(key) unless key.is_a?(OpenSSL::PKey::RSA)
+
+ @key = key.to_s
+ @uuid = uuid
+ rescue OpenSSL::PKey::PKeyError
+ raise ArgumentError, 'Invalid public key'
+ end
+
+ def save!
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(self.class.redis_key(uuid), key, ex: REDIS_EXPIRY_TIME)
+ end
+
+ self
+ end
+
+ def self.redis_key(uuid)
+ "JiraConnect:public_key:uuid=#{uuid}"
+ end
+ end
+end
diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb
index 0a2d3ba0749..23813fa138f 100644
--- a/app/models/jira_connect_installation.rb
+++ b/app/models/jira_connect_installation.rb
@@ -21,6 +21,9 @@ class JiraConnectInstallation < ApplicationRecord
})
}
+ scope :direct_installations, -> { joins(:subscriptions) }
+ scope :proxy_installations, -> { where.not(instance_url: nil) }
+
def client
Atlassian::JiraConnect::Client.new(base_url, shared_secret)
end
@@ -30,4 +33,20 @@ class JiraConnectInstallation < ApplicationRecord
instance_url
end
+
+ def audience_url
+ return unless proxy?
+
+ Gitlab::Utils.append_path(instance_url, '/-/jira_connect')
+ end
+
+ def audience_installed_event_url
+ return unless proxy?
+
+ Gitlab::Utils.append_path(instance_url, '/-/jira_connect/events/installed')
+ end
+
+ def proxy?
+ instance_url.present?
+ end
end
diff --git a/app/models/jira_import_state.rb b/app/models/jira_import_state.rb
index 76b5f1def6a..97d6cd00fb8 100644
--- a/app/models/jira_import_state.rb
+++ b/app/models/jira_import_state.rb
@@ -24,7 +24,7 @@ class JiraImportState < ApplicationRecord
validates :project, uniqueness: {
conditions: -> { where.not(status: STATUSES.values_at(:failed, :finished)) },
- message: _('Cannot have multiple Jira imports running at the same time')
+ message: N_('Cannot have multiple Jira imports running at the same time')
}
before_save :ensure_error_message_size
diff --git a/app/models/label.rb b/app/models/label.rb
index 6608a0573cb..483d51099b1 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -42,6 +42,7 @@ class Label < ApplicationRecord
scope :order_name_asc, -> { reorder(title: :asc) }
scope :order_name_desc, -> { reorder(title: :desc) }
scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) }
+ scope :with_preloaded_container, -> { preload(parent_container: :route) }
scope :top_labels_by_target, -> (target_relation) {
label_id_column = arel_table[:id]
@@ -59,6 +60,13 @@ class Label < ApplicationRecord
.distinct
}
+ scope :for_targets, ->(target_relation) do
+ joins(:label_links)
+ .merge(LabelLink.where(target: target_relation))
+ .select(arel_table[Arel.star], LabelLink.arel_table[:target_id])
+ .with_preloaded_container
+ end
+
def self.prioritized(project)
joins(:priorities)
.where(label_priorities: { project_id: project })
diff --git a/app/models/member.rb b/app/models/member.rb
index c5351d5447b..ff1d8f18c25 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -55,7 +55,7 @@ class Member < ApplicationRecord
validate :signup_email_valid?, on: :create, if: ->(member) { member.invite_email.present? }
validates :user_id,
uniqueness: {
- message: _('project bots cannot be added to other groups / projects')
+ message: N_('project bots cannot be added to other groups / projects')
},
if: :project_bot?
validate :access_level_inclusion
@@ -627,7 +627,6 @@ class Member < ApplicationRecord
end
def blocking_refresh
- return true unless Feature.enabled?(:allow_non_blocking_member_refresh)
return true if @blocking_refresh.nil?
@blocking_refresh
diff --git a/app/models/members/member_role.rb b/app/models/members/member_role.rb
index 2e8532fa739..b4e3d6874ef 100644
--- a/app/models/members/member_role.rb
+++ b/app/models/members/member_role.rb
@@ -4,6 +4,15 @@ class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass
has_many :members
belongs_to :namespace
- validates :namespace_id, presence: true
+ validates :namespace, presence: true
validates :base_access_level, presence: true
+ validate :belongs_to_top_level_namespace
+
+ private
+
+ def belongs_to_top_level_namespace
+ return if !namespace || namespace.root?
+
+ errors.add(:namespace, s_("must be top-level namespace"))
+ end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a57cb97e936..fb20d91fa20 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -41,8 +41,6 @@ class MergeRequest < ApplicationRecord
'Ci::CompareCodequalityReportsService' => ->(project) { true }
}.freeze
- MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS = 100
-
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User"
@@ -73,6 +71,11 @@ class MergeRequest < ApplicationRecord
belongs_to :latest_merge_request_diff, class_name: 'MergeRequestDiff'
manual_inverse_association :latest_merge_request_diff, :merge_request
+ # method overriden in EE
+ def suggested_reviewer_users
+ User.none
+ end
+
# This is the same as latest_merge_request_diff unless:
# 1. There are arguments - in which case we might be trying to force-reload.
# 2. This association is already loaded.
@@ -238,6 +241,12 @@ class MergeRequest < ApplicationRecord
Gitlab::Timeless.timeless(merge_request, &block)
end
+ after_transition any => [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking, :can_be_merged, :cannot_be_merged] do |merge_request, transition|
+ if Feature.enabled?(:trigger_mr_subscription_on_merge_status_change, merge_request.project)
+ GraphqlTriggers.merge_request_merge_status_updated(merge_request)
+ end
+ end
+
# rubocop: disable CodeReuse/ServiceClass
after_transition [:unchecked, :checking] => :cannot_be_merged do |merge_request, transition|
if merge_request.notify_conflict?
@@ -269,7 +278,7 @@ class MergeRequest < ApplicationRecord
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_or_merged_without_fork?]
validate :validate_fork, unless: :closed_or_merged_without_fork?
validate :validate_target_project, on: :create
- validate :validate_reviewer_and_assignee_size_length, unless: :importing?
+ validate :validate_reviewer_size_length, unless: :importing?
scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
@@ -438,6 +447,7 @@ class MergeRequest < ApplicationRecord
# we'd eventually rename the column for avoiding confusions, but in the mean time
# please use `auto_merge_enabled` alias instead of `merge_when_pipeline_succeeds`.
alias_attribute :auto_merge_enabled, :merge_when_pipeline_succeeds
+ alias_attribute :issuing_parent_id, :target_project_id
alias_method :issuing_parent, :target_project
delegate :builds_with_coverage, to: :head_pipeline, prefix: true, allow_nil: true
@@ -602,7 +612,7 @@ class MergeRequest < ApplicationRecord
end
def self.participant_includes
- [:reviewers, :award_emoji] + super
+ [:assignees, :reviewers] + super
end
def committers
@@ -988,18 +998,12 @@ class MergeRequest < ApplicationRecord
'Source project is not a fork of the target project'
end
- def self.max_number_of_assignees_or_reviewers_message
- # Assignees will be included in https://gitlab.com/gitlab-org/gitlab/-/issues/368936
- _("total must be less than or equal to %{size}") % { size: MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS }
- end
-
- def validate_reviewer_and_assignee_size_length
- # Assigness will be added in a subsequent MR https://gitlab.com/gitlab-org/gitlab/-/issues/368936
+ def validate_reviewer_size_length
return true unless Feature.enabled?(:limit_reviewer_and_assignee_size)
return true unless reviewers.size > MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
errors.add :reviewers,
- -> (_object, _data) { MergeRequest.max_number_of_assignees_or_reviewers_message }
+ -> (_object, _data) { self.class.max_number_of_assignees_or_reviewers_message }
end
def merge_ongoing?
@@ -1989,6 +1993,10 @@ class MergeRequest < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
end
+ def can_suggest_reviewers?
+ false # overridden in EE
+ end
+
private
attr_accessor :skip_fetch_ref
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index 36902e43a77..04b322ef5a6 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -25,6 +25,10 @@ class MergeRequestDiffFile < ApplicationRecord
return '' if fetched_diff.blank?
encode_utf8(fetched_diff) if fetched_diff.respond_to?(:encoding)
+ rescue StandardError => e
+ log_exception('Failed fetching merge request diff', e)
+
+ ''
end
def diff
@@ -75,15 +79,19 @@ class MergeRequestDiffFile < ApplicationRecord
content
rescue StandardError => e
+ log_exception('Cached external diff export failed', e)
+
+ diff
+ end
+
+ def log_exception(message, exception)
log_payload = {
- message: 'Cached external diff export failed',
+ message: message,
merge_request_diff_file_id: id,
merge_request_diff_id: merge_request_diff&.id
}
- Gitlab::ExceptionLogFormatter.format!(e, log_payload)
+ Gitlab::ExceptionLogFormatter.format!(exception, log_payload)
Gitlab::AppLogger.warn(log_payload)
-
- diff
end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index ff4fadb0f13..da07d8dd9fc 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
class Milestone < ApplicationRecord
+ include AtomicInternalId
include Sortable
include Timebox
include Milestoneish
include FromUnion
include Importable
+ include IidRoutes
prepend_mod_with('Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
@@ -13,6 +15,9 @@ class Milestone < ApplicationRecord
ALL = [::Timebox::None, ::Timebox::Any, ::Timebox::Started, ::Timebox::Upcoming].freeze
end
+ belongs_to :project
+ belongs_to :group
+
has_many :milestone_releases
has_many :releases, through: :milestone_releases
@@ -30,13 +35,28 @@ class Milestone < ApplicationRecord
.order(:project_id, :group_id, :due_date)
end
+ scope :of_projects, ->(ids) { where(project_id: ids) }
+ scope :for_projects, -> { where(group: nil).includes(:project) }
+ scope :for_projects_and_groups, -> (projects, groups) do
+ projects = projects.compact if projects.is_a? Array
+ projects = [] if projects.nil?
+
+ groups = groups.compact if groups.is_a? Array
+ groups = [] if groups.nil?
+
+ from_union([where(project_id: projects), where(group_id: groups)], remove_duplicates: false)
+ end
+
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(arel_table[:due_date].asc.nulls_last) }
scope :with_api_entity_associations, -> { preload(project: [:project_feature, :route, namespace: :route]) }
scope :order_by_dates_and_title, -> { order(due_date: :asc, start_date: :asc, title: :asc) }
+ validates :group, presence: true, unless: :project
+ validates :project, presence: true, unless: :group
validates :title, presence: true
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
+ validate :parent_type_check
validate :uniqueness_of_title, if: :title_changed?
state_machine :state, initial: :active do
@@ -176,10 +196,18 @@ class Milestone < ApplicationRecord
# TODO: remove after all code paths use `timebox_id`
# https://gitlab.com/gitlab-org/gitlab/-/issues/215688
alias_method :milestoneish_id, :timebox_id
- # TODO: remove after all code paths use (group|project)_timebox?
- # https://gitlab.com/gitlab-org/gitlab/-/issues/215690
- alias_method :group_milestone?, :group_timebox?
- alias_method :project_milestone?, :project_timebox?
+
+ def group_milestone?
+ group_id.present?
+ end
+
+ def project_milestone?
+ project_id.present?
+ end
+
+ def resource_parent
+ group || project
+ end
def parent
if group_milestone?
@@ -193,8 +221,63 @@ class Milestone < ApplicationRecord
group_milestone? && parent.subgroup?
end
+ def merge_requests_enabled?
+ if group_milestone?
+ # Assume that groups have at least one project with merge requests enabled.
+ # Otherwise, we would need to load all of the projects from the database.
+ true
+ elsif project_milestone?
+ project&.merge_requests_enabled?
+ end
+ end
+
+ ##
+ # Returns the String necessary to reference a milestone in Markdown. Group
+ # milestones only support name references, and do not support cross-project
+ # references.
+ #
+ # format - Symbol format to use (default: :iid, optional: :name)
+ #
+ # Examples:
+ #
+ # Milestone.first.to_reference # => "%1"
+ # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-foss%1"
+ #
+ def to_reference(from = nil, format: :name, full: false)
+ format_reference = timebox_format_reference(format)
+ reference = "#{self.class.reference_prefix}#{format_reference}"
+
+ if project
+ "#{project.to_reference_base(from, full: full)}#{reference}"
+ else
+ reference
+ end
+ end
+
private
+ def timebox_format_reference(format = :iid)
+ raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format)
+
+ if group_milestone? && format == :iid
+ raise ArgumentError, _('Cannot refer to a group milestone by an internal id!')
+ end
+
+ if format == :name && !name.include?('"')
+ %("#{name}")
+ else
+ iid
+ end
+ end
+
+ # Milestone should be either a project milestone or a group milestone
+ def parent_type_check
+ return unless group_id && project_id
+
+ field = project_id_changed? ? :project_id : :group_id
+ errors.add(field, _("milestone should belong either to a project or a group.") % { timebox_name: timebox_name })
+ end
+
def issues_finder_params
{ project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact
end
diff --git a/app/models/ml/candidate_param.rb b/app/models/ml/candidate_param.rb
index cbdddcc8a1a..a259e059379 100644
--- a/app/models/ml/candidate_param.rb
+++ b/app/models/ml/candidate_param.rb
@@ -3,6 +3,7 @@
module Ml
class CandidateParam < ApplicationRecord
validates :candidate, presence: true
+ validates :name, uniqueness: { scope: :candidate }
validates :name, :value, length: { maximum: 250 }, presence: true
belongs_to :candidate, class_name: 'Ml::Candidate'
diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb
index e4e9baac4c8..a32099e8a0c 100644
--- a/app/models/ml/experiment.rb
+++ b/app/models/ml/experiment.rb
@@ -13,10 +13,6 @@ module Ml
has_internal_id :iid, scope: :project
- def artifact_location
- 'not_implemented'
- end
-
class << self
def by_project_id_and_iid(project_id, iid)
find_by(project_id: project_id, iid: iid)
@@ -26,8 +22,8 @@ module Ml
find_by(project_id: project_id, name: name)
end
- def has_record?(project_id, name)
- where(project_id: project_id, name: name).exists?
+ def by_project_id(project_id)
+ where(project_id: project_id)
end
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 0ffd5c446d3..42f362876bb 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -130,6 +130,10 @@ class Namespace < ApplicationRecord
to: :namespace_settings, allow_nil: true
delegate :show_diff_preview_in_email, :show_diff_preview_in_email?, :show_diff_preview_in_email=,
to: :namespace_settings
+ delegate :maven_package_requests_forwarding,
+ :pypi_package_requests_forwarding,
+ :npm_package_requests_forwarding,
+ to: :package_settings
after_save :reload_namespace_details
diff --git a/app/models/namespace/aggregation_schedule.rb b/app/models/namespace/aggregation_schedule.rb
index ed61c807519..cd7d4fc409a 100644
--- a/app/models/namespace/aggregation_schedule.rb
+++ b/app/models/namespace/aggregation_schedule.rb
@@ -6,13 +6,20 @@ class Namespace::AggregationSchedule < ApplicationRecord
self.primary_key = :namespace_id
- DEFAULT_LEASE_TIMEOUT = 1.5.hours.to_i
REDIS_SHARED_KEY = 'gitlab:update_namespace_statistics_delay'
belongs_to :namespace
after_create :schedule_root_storage_statistics
+ def self.default_lease_timeout
+ if Feature.enabled?(:remove_namespace_aggregator_delay)
+ 30.minutes.to_i
+ else
+ 1.hour.to_i
+ end
+ end
+
def schedule_root_storage_statistics
run_after_commit_or_now do
try_obtain_lease do
@@ -20,7 +27,7 @@ class Namespace::AggregationSchedule < ApplicationRecord
.perform_async(namespace_id)
Namespaces::RootStatisticsWorker
- .perform_in(DEFAULT_LEASE_TIMEOUT, namespace_id)
+ .perform_in(self.class.default_lease_timeout, namespace_id)
end
end
end
@@ -29,7 +36,7 @@ class Namespace::AggregationSchedule < ApplicationRecord
# Used by ExclusiveLeaseGuard
def lease_timeout
- DEFAULT_LEASE_TIMEOUT
+ self.class.default_lease_timeout
end
# Used by ExclusiveLeaseGuard
diff --git a/app/models/namespace/detail.rb b/app/models/namespace/detail.rb
index dbbf9f4944a..a5643ab9f79 100644
--- a/app/models/namespace/detail.rb
+++ b/app/models/namespace/detail.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class Namespace::Detail < ApplicationRecord
+ include IgnorableColumns
+
+ ignore_column :free_user_cap_over_limt_notified_at, remove_with: '15.7', remove_after: '2022-11-22'
+
belongs_to :namespace, inverse_of: :namespace_details
validates :namespace, presence: true
validates :description, length: { maximum: 255 }
diff --git a/app/models/namespace/package_setting.rb b/app/models/namespace/package_setting.rb
index 881b2f3acb3..22c3e41ff21 100644
--- a/app/models/namespace/package_setting.rb
+++ b/app/models/namespace/package_setting.rb
@@ -1,9 +1,15 @@
# frozen_string_literal: true
class Namespace::PackageSetting < ApplicationRecord
+ include CascadingNamespaceSettingAttribute
+
self.primary_key = :namespace_id
self.table_name = 'namespace_package_settings'
+ cascading_attr :maven_package_requests_forwarding
+ cascading_attr :npm_package_requests_forwarding
+ cascading_attr :pypi_package_requests_forwarding
+
PackageSettingNotImplemented = Class.new(StandardError)
PACKAGES_WITH_SETTINGS = %w[maven generic].freeze
diff --git a/app/models/note.rb b/app/models/note.rb
index daac489757b..e444111119b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -22,6 +22,7 @@ class Note < ApplicationRecord
include ThrottledTouch
include FromUnion
include Sortable
+ include EachBatch
ISSUE_TASK_SYSTEM_NOTE_PATTERN = /\A.*marked\sthe\stask.+as\s(completed|incomplete).*\z/.freeze
@@ -693,7 +694,7 @@ class Note < ApplicationRecord
# Method necesary while we transition into the new format for task system notes
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/369923
def note
- return super unless system? && for_issue? && super.match?(ISSUE_TASK_SYSTEM_NOTE_PATTERN)
+ return super unless system? && for_issue? && super&.match?(ISSUE_TASK_SYSTEM_NOTE_PATTERN)
super.sub!('task', 'checklist item')
end
@@ -701,11 +702,15 @@ class Note < ApplicationRecord
# Method necesary while we transition into the new format for task system notes
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/369923
def note_html
- return super unless system? && for_issue? && super.match?(ISSUE_TASK_SYSTEM_NOTE_PATTERN)
+ return super unless system? && for_issue? && super&.match?(ISSUE_TASK_SYSTEM_NOTE_PATTERN)
super.sub!('task', 'checklist item')
end
+ def issuable_ability_name
+ confidential? ? :read_internal_note : :read_note
+ end
+
private
def system_note_viewable_by?(user)
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index caa24377791..20d5a5ae1a1 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -97,8 +97,6 @@ class NotificationRecipient
end
def email_blocked?
- return false if Feature.disabled?(:block_emails_with_failures)
-
recipient_email = user.notification_email_for(@group)
Gitlab::ApplicationRateLimiter.peek(:permanent_email_failure, scope: recipient_email) ||
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index b4c09d99bb0..317db51f4ef 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -114,13 +114,18 @@ class Packages::Package < ApplicationRecord
)
end
+ scope :with_case_insensitive_version, ->(version) do
+ where('LOWER(version) = ?', version.downcase)
+ end
+
scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) }
scope :with_version, ->(version) { where(version: version) }
scope :without_version_like, -> (version) { where.not(arel_table[:version].matches(version)) }
scope :with_package_type, ->(package_type) { where(package_type: package_type) }
scope :without_package_type, ->(package_type) { where.not(package_type: package_type) }
scope :displayable, -> { with_status(DISPLAYABLE_STATUSES) }
- scope :including_project_route, -> { includes(project: { namespace: :route }) }
+ scope :including_project_route, -> { includes(project: :route) }
+ scope :including_project_namespace_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) }
scope :including_dependency_links, -> { includes(dependency_links: :dependency) }
diff --git a/app/models/packages/rpm/repository_file.rb b/app/models/packages/rpm/repository_file.rb
new file mode 100644
index 00000000000..4b5fa59c6ee
--- /dev/null
+++ b/app/models/packages/rpm/repository_file.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+module Packages
+ module Rpm
+ class RepositoryFile < ApplicationRecord
+ include EachBatch
+ include UpdateProjectStatistics
+ include FileStoreMounter
+ include Packages::Installable
+
+ INSTALLABLE_STATUSES = [:default].freeze
+
+ enum status: { default: 0, pending_destruction: 1, processing: 2, error: 3 }
+
+ belongs_to :project, inverse_of: :repository_files
+
+ validates :project, presence: true
+ validates :file, presence: true
+ validates :file_name, presence: true
+
+ mount_file_store_uploader Packages::Rpm::RepositoryFileUploader
+
+ update_project_statistics project_statistics_name: :packages_size
+ end
+ end
+end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index e7d455085c0..c1056d4f6cb 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -32,7 +32,9 @@ module Pages
{
type: 'zip',
- path: deployment.file.url_or_file_path(expire_at: 1.day.from_now),
+ path: deployment.file.url_or_file_path(
+ expire_at: ::Gitlab::Pages::CacheControl::DEPLOYMENT_EXPIRATION.from_now
+ ),
global_id: global_id,
sha256: deployment.file_sha256,
file_size: deployment.size,
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 9ed25c56ed6..f0ed1822da6 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -5,6 +5,8 @@ class PersonalAccessToken < ApplicationRecord
include TokenAuthenticatable
include Sortable
include EachBatch
+ include CreatedAtFilterable
+ include Gitlab::SQL::Pattern
extend ::Gitlab::Utils::Override
add_authentication_token_field :token, digest: true
@@ -24,7 +26,6 @@ class PersonalAccessToken < ApplicationRecord
scope :expiring_and_not_notified, ->(date) { where(["revoked = false AND expire_notification_delivered = false AND expires_at >= CURRENT_DATE AND expires_at <= ?", date]) }
scope :expired_today_and_not_notified, -> { where(["revoked = false AND expires_at = CURRENT_DATE AND after_expiry_notification_delivered = false"]) }
scope :inactive, -> { where("revoked = true OR expires_at < CURRENT_DATE") }
- scope :created_before, -> (date) { where("personal_access_tokens.created_at < :date", date: date) }
scope :last_used_before_or_unused, -> (date) { where("personal_access_tokens.created_at < :date AND (last_used_at < :date OR last_used_at IS NULL)", date: date) }
scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
@@ -38,6 +39,8 @@ class PersonalAccessToken < ApplicationRecord
scope :order_expires_at_asc_id_desc, -> { reorder(expires_at: :asc, id: :desc) }
scope :project_access_token, -> { includes(:user).where(user: { user_type: :project_bot }) }
scope :owner_is_human, -> { includes(:user).where(user: { user_type: :human }) }
+ scope :last_used_before, -> (date) { where("last_used_at <= ?", date) }
+ scope :last_used_after, -> (date) { where("last_used_at >= ?", date) }
validates :scopes, presence: true
validate :validate_scopes
@@ -90,6 +93,10 @@ class PersonalAccessToken < ApplicationRecord
Gitlab::CurrentSettings.current_application_settings.personal_access_token_prefix
end
+ def self.search(query)
+ fuzzy_search(query, [:name])
+ end
+
override :format_token
def format_token(token)
"#{self.class.token_prefix}#{token}"
diff --git a/app/models/preloaders/labels_preloader.rb b/app/models/preloaders/labels_preloader.rb
index 722d588d8bc..b6e73c1cd02 100644
--- a/app/models/preloaders/labels_preloader.rb
+++ b/app/models/preloaders/labels_preloader.rb
@@ -21,8 +21,10 @@ module Preloaders
def preload_all
preloader = ActiveRecord::Associations::Preloader.new
+ preloader.preload(labels, parent_container: :route)
preloader.preload(labels.select { |l| l.is_a? ProjectLabel }, { project: [:project_feature, namespace: :route] })
preloader.preload(labels.select { |l| l.is_a? GroupLabel }, { group: :route })
+
labels.each do |label|
label.lazy_subscription(user)
label.lazy_subscription(user, project) if project.present?
diff --git a/app/models/preloaders/project_root_ancestor_preloader.rb b/app/models/preloaders/project_root_ancestor_preloader.rb
index 8d04e71774c..1e935249407 100644
--- a/app/models/preloaders/project_root_ancestor_preloader.rb
+++ b/app/models/preloaders/project_root_ancestor_preloader.rb
@@ -21,7 +21,8 @@ module Preloaders
ActiveRecord::Associations::Preloader.new.preload(@projects, :namespace)
@projects.each do |project|
- project.namespace.root_ancestor = root_ancestors_by_id[project.id]&.first
+ root_ancestor = root_ancestors_by_id[project.id]&.first
+ project.namespace.root_ancestor = root_ancestor if root_ancestor.present?
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index c5fad189f87..7b61010ab01 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -32,7 +32,6 @@ class Project < ApplicationRecord
include FeatureGate
include OptionallySearch
include FromUnion
- include IgnorableColumns
include Repositories::CanHousekeepRepository
include EachBatch
include GitlabRoutingHelper
@@ -49,8 +48,6 @@ class Project < ApplicationRecord
BoardLimitExceeded = Class.new(StandardError)
ExportLimitExceeded = Class.new(StandardError)
- ignore_columns :build_coverage_regex, remove_after: '2022-10-22', remove_with: '15.5'
-
STATISTICS_ATTRIBUTE = 'repositories_count'
UNKNOWN_IMPORT_URL = 'http://unknown.git'
# Hashed Storage versions handle rolling out new storage to project and dependents models:
@@ -239,6 +236,9 @@ class Project < ApplicationRecord
# Packages
has_many :packages, class_name: 'Packages::Package'
has_many :package_files, through: :packages, class_name: 'Packages::PackageFile'
+ # repository_files must be destroyed by ruby code in order to properly remove carrierwave uploads
+ has_many :repository_files, inverse_of: :project, class_name: 'Packages::Rpm::RepositoryFile',
+ dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::ProjectDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :packages_cleanup_policy, class_name: 'Packages::Cleanup::Policy', inverse_of: :project
@@ -262,11 +262,11 @@ class Project < ApplicationRecord
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues
has_many :incident_management_issuable_escalation_statuses, through: :issues, inverse_of: :project, class_name: 'IncidentManagement::IssuableEscalationStatus'
+ has_many :incident_management_timeline_event_tags, inverse_of: :project, class_name: 'IncidentManagement::TimelineEventTag'
has_many :labels, class_name: 'ProjectLabel'
has_many :integrations
has_many :events
has_many :milestones
- has_many :iterations
# Projects with a very large number of notes may time out destroying them
# through the foreign key. Additionally, the deprecated attachment uploader
@@ -353,6 +353,7 @@ class Project < ApplicationRecord
has_many :stages, class_name: 'Ci::Stage', inverse_of: :project
has_many :ci_refs, class_name: 'Ci::Ref', inverse_of: :project
+ has_many :pipeline_metadata, class_name: 'Ci::PipelineMetadata', inverse_of: :project
has_many :pending_builds, class_name: 'Ci::PendingBuild'
has_many :builds, class_name: 'Ci::Build', inverse_of: :project
has_many :processables, class_name: 'Ci::Processable', inverse_of: :project
@@ -476,7 +477,8 @@ class Project < ApplicationRecord
delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true
delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
delegate :forward_deployment_enabled, :forward_deployment_enabled=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
- delegate :job_token_scope_enabled, :job_token_scope_enabled=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
+ delegate :job_token_scope_enabled, :job_token_scope_enabled=, to: :ci_cd_settings, prefix: :ci_outbound, allow_nil: true
+ delegate :inbound_job_token_scope_enabled, :inbound_job_token_scope_enabled=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
delegate :keep_latest_artifact, :keep_latest_artifact=, to: :ci_cd_settings, allow_nil: true
delegate :opt_in_jwt, :opt_in_jwt=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
delegate :allow_fork_pipelines_to_run_in_parent_project, :allow_fork_pipelines_to_run_in_parent_project=, to: :ci_cd_settings, prefix: :ci, allow_nil: true
@@ -492,12 +494,17 @@ class Project < ApplicationRecord
delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
+ delegate :maven_package_requests_forwarding,
+ :pypi_package_requests_forwarding,
+ :npm_package_requests_forwarding,
+ to: :namespace
+
# Validations
validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true
validates :ci_config_path,
format: { without: %r{(\.{2}|\A/)},
- message: _('cannot include leading slash or directory traversal.') },
+ message: N_('cannot include leading slash or directory traversal.') },
length: { maximum: 255 },
allow_blank: true
validates :name,
@@ -693,13 +700,13 @@ class Project < ApplicationRecord
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout,
- default: 3600, error_message: _('Maximum job timeout has a value which could not be accepted')
+ default: 3600, error_message: N_('Maximum job timeout has a value which could not be accepted')
validates :build_timeout, allow_nil: true,
numericality: { greater_than_or_equal_to: 10.minutes,
less_than: MAX_BUILD_TIMEOUT,
only_integer: true,
- message: _('needs to be between 10 minutes and 1 month') }
+ message: N_('needs to be between 10 minutes and 1 month') }
# Used by Projects::CleanupService to hold a map of rewritten object IDs
mount_uploader :bfg_object_map, AttachmentUploader
@@ -1280,6 +1287,8 @@ class Project < ApplicationRecord
valid?(:import_url) || errors.messages[:import_url].nil?
end
+ # TODO: rename to build_or_assign_import_data as it doesn't save record
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/377319
def create_or_update_import_data(data: nil, credentials: nil)
return if data.nil? && credentials.nil?
@@ -2720,6 +2729,7 @@ class Project < ApplicationRecord
ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci]
end
+ # DO NOT USE. This method will be deprecated soon
def uses_external_project_ci_config?
!!(ci_config_path =~ %r{@.+/.+})
end
@@ -2844,6 +2854,7 @@ class Project < ApplicationRecord
repository.gitlab_ci_yml_for(sha, ci_config_path_or_default)
end
+ # DO NOT USE. This method will be deprecated soon
def ci_config_external_project
Project.find_by_full_path(ci_config_path.split('@', 2).last)
end
@@ -2886,12 +2897,18 @@ class Project < ApplicationRecord
ci_cd_settings.allow_fork_pipelines_to_run_in_parent_project?
end
- def ci_job_token_scope_enabled?
+ def ci_outbound_job_token_scope_enabled?
return false unless ci_cd_settings
ci_cd_settings.job_token_scope_enabled?
end
+ def ci_inbound_job_token_scope_enabled?
+ return false unless ci_cd_settings
+
+ ci_cd_settings.inbound_job_token_scope_enabled?
+ end
+
def restrict_user_defined_variables?
return false unless ci_cd_settings
@@ -2939,12 +2956,6 @@ class Project < ApplicationRecord
end
end
- def remove_project_authorizations(user_ids, per_batch = 1000)
- user_ids.each_slice(per_batch) do |user_ids_batch|
- project_authorizations.where(user_id: user_ids_batch).delete_all
- end
- end
-
def enforced_runner_token_expiration_interval
all_parent_groups = Gitlab::ObjectHierarchy.new(Group.where(id: group)).base_and_ancestors
all_group_settings = NamespaceSetting.where(namespace_id: all_parent_groups)
@@ -3023,11 +3034,7 @@ class Project < ApplicationRecord
end
def packages_policy_subject
- if Feature.enabled?(:read_package_policy_rule, group)
- ::Packages::Policies::Project.new(self)
- else
- self
- end
+ ::Packages::Policies::Project.new(self)
end
def destroy_deployment_by_id(deployment_id)
@@ -3040,6 +3047,16 @@ class Project < ApplicationRecord
pages_domains.count < Gitlab::CurrentSettings.max_pages_custom_domains_per_project
end
+ # overridden in EE
+ def can_suggest_reviewers?
+ false
+ end
+
+ # overridden in EE
+ def suggested_reviewers_available?
+ false
+ end
+
private
# overridden in EE
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index 5c6fdec16ca..8b43e5e5d63 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
class ProjectAuthorization < ApplicationRecord
+ BATCH_SIZE = 1000
+ SLEEP_DELAY = 0.1
+
extend SuppressCompositePrimaryKeyWarning
include FromUnion
@@ -26,11 +29,45 @@ class ProjectAuthorization < ApplicationRecord
super(attributes, unique_by: connection.schema_cache.primary_keys(table_name))
end
- def self.insert_all_in_batches(attributes, per_batch = 1000)
+ def self.insert_all_in_batches(attributes, per_batch = BATCH_SIZE)
+ add_delay = add_delay_between_batches?(entire_size: attributes.size, batch_size: per_batch)
+
attributes.each_slice(per_batch) do |attributes_batch|
insert_all(attributes_batch)
+ perform_delay if add_delay
+ end
+ end
+
+ def self.delete_all_in_batches_for_project(project:, user_ids:, per_batch: BATCH_SIZE)
+ add_delay = add_delay_between_batches?(entire_size: user_ids.size, batch_size: per_batch)
+
+ user_ids.each_slice(per_batch) do |user_ids_batch|
+ project.project_authorizations.where(user_id: user_ids_batch).delete_all
+ perform_delay if add_delay
+ end
+ end
+
+ def self.delete_all_in_batches_for_user(user:, project_ids:, per_batch: BATCH_SIZE)
+ add_delay = add_delay_between_batches?(entire_size: project_ids.size, batch_size: per_batch)
+
+ project_ids.each_slice(per_batch) do |project_ids_batch|
+ user.project_authorizations.where(project_id: project_ids_batch).delete_all
+ perform_delay if add_delay
end
end
+
+ private_class_method def self.add_delay_between_batches?(entire_size:, batch_size:)
+ # The reason for adding a delay is to give the replica database enough time to
+ # catch up with the primary when large batches of records are being added/removed.
+ # Hance, we add a delay only if the GitLab installation has a replica database configured.
+ entire_size > batch_size &&
+ !::Gitlab::Database::LoadBalancing.primary_only? &&
+ Feature.enabled?(:enable_minor_delay_during_project_authorizations_refresh)
+ end
+
+ private_class_method def self.perform_delay
+ sleep(SLEEP_DELAY)
+ end
end
ProjectAuthorization.prepend_mod_with('ProjectAuthorization')
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index 38740aa20dd..d7a5d0d9d84 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -22,10 +22,6 @@ class ProjectCiCdSetting < ApplicationRecord
chronic_duration_attr :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval
- def forward_deployment_enabled?
- super && ::Feature.enabled?(:forward_deployment_enabled, project)
- end
-
def keep_latest_artifacts_available?
# The project level feature can only be enabled when the feature is enabled instance wide
Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact? && keep_latest_artifact?
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index 2ba3c74df5b..9f9447c1de2 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -9,7 +9,7 @@ class ProjectGroupLink < ApplicationRecord
validates :project_id, presence: true
validates :group, presence: true
- validates :group_id, uniqueness: { scope: [:project_id], message: _("already shared with this group") }
+ validates :group_id, uniqueness: { scope: [:project_id], message: N_("already shared with this group") }
validates :group_access, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group
diff --git a/app/models/project_label.rb b/app/models/project_label.rb
index d0b16cc98b4..dc647901b46 100644
--- a/app/models/project_label.rb
+++ b/app/models/project_label.rb
@@ -4,6 +4,7 @@ class ProjectLabel < Label
MAX_NUMBER_OF_PRIORITIES = 1
belongs_to :project
+ belongs_to :parent_container, foreign_key: :project_id, class_name: 'Project'
validates :project, presence: true
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index f5c346eda30..6d40544fad4 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -21,6 +21,7 @@ class ProjectSetting < ApplicationRecord
validates :merge_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH }
validates :squash_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH }
validates :target_platforms, inclusion: { in: ALLOWED_TARGET_PLATFORMS }
+ validates :suggested_reviewers_enabled, inclusion: { in: [true, false] }
validate :validates_mr_default_target_self
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index a91e0291438..f108e43015e 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -27,6 +27,16 @@ class ProjectStatistics < ApplicationRecord
snippets_size: %i[storage_size]
}.freeze
NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size, :uploads_size, :container_registry_size].freeze
+ STORAGE_SIZE_COMPONENTS = [
+ :repository_size,
+ :wiki_size,
+ :lfs_objects_size,
+ :build_artifacts_size,
+ :packages_size,
+ :snippets_size,
+ :pipeline_artifacts_size,
+ :uploads_size
+ ].freeze
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
@@ -39,17 +49,18 @@ class ProjectStatistics < ApplicationRecord
def refresh!(only: [])
return if Gitlab::Database.read_only?
- COLUMNS_TO_REFRESH.each do |column, generator|
- if only.empty? || only.include?(column)
- public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend
- end
+ columns_to_update = only.empty? ? COLUMNS_TO_REFRESH : COLUMNS_TO_REFRESH & only
+ columns_to_update.each do |column|
+ public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend
end
if only.empty? || only.any? { |column| NAMESPACE_RELATABLE_COLUMNS.include?(column) }
schedule_namespace_aggregation_worker
end
- save!
+ detect_race_on_record(log_fields: { caller: __method__, attributes: columns_to_update }) do
+ save!
+ end
end
def update_commit_count
@@ -97,21 +108,13 @@ class ProjectStatistics < ApplicationRecord
end
def update_storage_size
- storage_size = repository_size +
- wiki_size +
- lfs_objects_size +
- build_artifacts_size +
- packages_size +
- snippets_size +
- pipeline_artifacts_size +
- uploads_size
-
- self.storage_size = storage_size
+ self.storage_size = storage_size_components.sum { |component| method(component).call }
end
def refresh_storage_size!
- update_storage_size
- save!
+ detect_race_on_record(log_fields: { caller: __method__, attributes: :storage_size }) do
+ update!(storage_size: storage_size_sum)
+ end
end
# Since this incremental update method does not call update_storage_size above through before_save,
@@ -129,35 +132,41 @@ class ProjectStatistics < ApplicationRecord
if counter_attribute_enabled?(key)
project_statistics.delayed_increment_counter(key, amount)
else
- legacy_increment_statistic(project, key, amount)
+ project_statistics.legacy_increment_statistic(key, amount)
end
end
end
- def self.legacy_increment_statistic(project, key, amount)
- where(project_id: project.id).columns_to_increment(key, amount)
+ def self.incrementable_attribute?(key)
+ INCREMENTABLE_COLUMNS.key?(key) || counter_attribute_enabled?(key)
+ end
+
+ def legacy_increment_statistic(key, amount)
+ increment_columns!(key, amount)
Namespaces::ScheduleAggregationWorker.perform_async( # rubocop: disable CodeReuse/Worker
project.namespace_id)
end
- def self.columns_to_increment(key, amount)
- updates = ["#{key} = COALESCE(#{key}, 0) + (#{amount})"]
-
- if (additional = INCREMENTABLE_COLUMNS[key])
- additional.each do |column|
- updates << "#{column} = COALESCE(#{column}, 0) + (#{amount})"
- end
- end
+ private
- update_all(updates.join(', '))
+ def storage_size_components
+ STORAGE_SIZE_COMPONENTS
end
- def self.incrementable_attribute?(key)
- INCREMENTABLE_COLUMNS.key?(key) || counter_attribute_enabled?(key)
+ def storage_size_sum
+ storage_size_components.map { |component| "COALESCE (#{component}, 0)" }.join(' + ').freeze
end
- private
+ def increment_columns!(key, amount)
+ increments = { key => amount }
+ additional = INCREMENTABLE_COLUMNS.fetch(key, [])
+ additional.each do |column|
+ increments[column] = amount
+ end
+
+ update_counters_with_lease(increments)
+ end
def schedule_namespace_aggregation_worker
run_after_commit do
diff --git a/app/models/projects/build_artifacts_size_refresh.rb b/app/models/projects/build_artifacts_size_refresh.rb
index e66e1d5b42f..2ffc7478178 100644
--- a/app/models/projects/build_artifacts_size_refresh.rb
+++ b/app/models/projects/build_artifacts_size_refresh.rb
@@ -80,9 +80,7 @@ module Projects
end
def reset_project_statistics!
- statistics = project.statistics
- statistics.update!(build_artifacts_size: 0)
- statistics.clear_counter!(:build_artifacts_size)
+ project.statistics.reset_counter!(:build_artifacts_size)
end
def next_batch(limit:)
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index b3a918d8952..dfd5c315f6e 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -95,6 +95,10 @@ class ProtectedBranch < ApplicationRecord
def self.downcase_humanized_name
name.underscore.humanize.downcase
end
+
+ def default_branch?
+ name == project.default_branch
+ end
end
ProtectedBranch.prepend_mod_with('ProtectedBranch')
diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb
index de240e40316..df75c557717 100644
--- a/app/models/protected_branch/merge_access_level.rb
+++ b/app/models/protected_branch/merge_access_level.rb
@@ -2,4 +2,6 @@
class ProtectedBranch::MergeAccessLevel < ApplicationRecord
include ProtectedBranchAccess
+ # default value for the access_level column
+ GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER
end
diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb
index 5248834a2f2..6076fab20b7 100644
--- a/app/models/protected_branch/push_access_level.rb
+++ b/app/models/protected_branch/push_access_level.rb
@@ -2,6 +2,8 @@
class ProtectedBranch::PushAccessLevel < ApplicationRecord
include ProtectedBranchAccess
+ # default value for the access_level column
+ GITLAB_DEFAULT_ACCESS_LEVEL = Gitlab::Access::MAINTAINER
belongs_to :deploy_key
diff --git a/app/models/repository.rb b/app/models/repository.rb
index ee1bea0e8d2..3413b3e3424 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -48,22 +48,19 @@ class Repository
# For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `commit_count` cache key.
CACHED_METHODS = %i(size commit_count readme_path contribution_guide
- changelog license_blob license_key gitignore
+ changelog license_blob license_licensee license_gitaly gitignore
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref merged_branch_names
has_visible_content? issue_template_names_hash merge_request_template_names_hash
user_defined_metrics_dashboard_paths xcode_project? has_ambiguous_refs?).freeze
- # Methods that use cache_method but only memoize the value
- MEMOIZED_CACHED_METHODS = %i(license).freeze
-
# Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
# the corresponding methods to call for refreshing caches.
METHOD_CACHES_FOR_FILE_TYPES = {
readme: %i(readme_path),
changelog: :changelog,
- license: %i(license_blob license_key license),
+ license: %i(license_blob license_licensee license_gitaly),
contributing: :contribution_guide,
gitignore: :gitignore,
gitlab_ci: :gitlab_ci_yml,
@@ -650,25 +647,30 @@ class Repository
cache_method :license_blob
def license_key
- return unless exists?
-
- raw_repository.license_short_name
+ license&.key
end
- cache_method :license_key
def license
- return unless license_key
+ if Feature.enabled?(:license_from_gitaly)
+ license_gitaly
+ else
+ license_licensee
+ end
+ end
- licensee_object = Licensee::License.new(license_key)
+ def license_licensee
+ return unless exists?
- return if licensee_object.name.blank?
+ raw_repository.license(false)
+ end
+ cache_method :license_licensee
- licensee_object
- rescue Licensee::InvalidLicense => e
- Gitlab::ErrorTracking.track_exception(e)
- nil
+ def license_gitaly
+ return unless exists?
+
+ raw_repository.license(true)
end
- memoize_method :license
+ cache_method :license_gitaly
def gitignore
file_on_head(:gitignore)
@@ -787,8 +789,8 @@ class Repository
Commit.order_by(collection: commits, order_by: order_by, sort: sort)
end
- def branch_names_contains(sha)
- raw_repository.branch_names_contains_sha(sha)
+ def branch_names_contains(sha, limit: 0)
+ raw_repository.branch_names_contains_sha(sha, limit: limit)
end
def tag_names_contains(sha, limit: 0)
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index 0a59d9cef9b..a1753df9294 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -115,7 +115,7 @@ class ResourceLabelEvent < ResourceEvent
end
def discussion_id_key
- [self.class.name, created_at, user_id]
+ [self.class.name, created_at.to_f, user_id]
end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 9b7c37dd23e..9ec685c5580 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -350,7 +350,7 @@ class Snippet < ApplicationRecord
end
def can_cache_field?(field)
- field != :content || MarkupHelper.gitlab_markdown?(file_name)
+ field != :content || Gitlab::MarkupHelper.gitlab_markdown?(file_name)
end
def hexdigest
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 941d0394b94..c6adf5c263c 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class Tree
- include Gitlab::MarkupHelper
include Gitlab::Utils::StrongMemoize
attr_accessor :repository, :sha, :path, :entries, :cursor
@@ -24,11 +23,11 @@ class Tree
end
previewable_readmes = available_readmes.select do |blob|
- previewable?(blob.name)
+ Gitlab::MarkupHelper.previewable?(blob.name)
end
plain_readmes = available_readmes.select do |blob|
- plain?(blob.name)
+ Gitlab::MarkupHelper.plain?(blob.name)
end
# Prioritize previewable over plain readmes
diff --git a/app/models/user.rb b/app/models/user.rb
index 3f07e1b1ec0..6d198fc755b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -60,7 +60,7 @@ class User < ApplicationRecord
default_value_for :admin, false
default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
- default_value_for :can_create_group, gitlab_config.default_can_create_group
+ default_value_for(:can_create_group) { Gitlab::CurrentSettings.can_create_group }
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false
@@ -79,6 +79,7 @@ class User < ApplicationRecord
otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
devise :two_factor_backupable, otp_number_of_backup_codes: 10
+ devise :two_factor_backupable_pbkdf2
serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiveRecordSerialize
devise :lockable, :recoverable, :rememberable, :trackable,
@@ -168,6 +169,10 @@ class User < ApplicationRecord
through: :group_members,
source: :group
alias_attribute :masters_groups, :maintainers_groups
+ has_many :developer_maintainer_owned_groups,
+ -> { where(members: { access_level: [Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
+ through: :group_members,
+ source: :group
has_many :reporter_developer_maintainer_owned_groups,
-> { where(members: { access_level: [Gitlab::Access::REPORTER, Gitlab::Access::DEVELOPER, Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) },
through: :group_members,
@@ -193,6 +198,10 @@ class User < ApplicationRecord
has_many :snippets, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notes, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issues, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :legacy_assigned_merge_requests, class_name: 'MergeRequest', dependent: :nullify, foreign_key: :assignee_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :merged_merge_requests, class_name: 'MergeRequest::Metrics', dependent: :nullify, foreign_key: :merged_by_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :closed_merge_requests, class_name: 'MergeRequest::Metrics', dependent: :nullify, foreign_key: :latest_closed_by_id # rubocop:disable Cop/ActiveRecordDependent
+ has_many :updated_merge_requests, class_name: 'MergeRequest', dependent: :nullify, foreign_key: :updated_by_id # rubocop:disable Cop/ActiveRecordDependent
has_many :updated_issues, class_name: 'Issue', dependent: :nullify, foreign_key: :updated_by_id # rubocop:disable Cop/ActiveRecordDependent
has_many :closed_issues, class_name: 'Issue', dependent: :nullify, foreign_key: :closed_by_id # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
@@ -205,14 +214,15 @@ class User < ApplicationRecord
has_many :spam_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :builds, class_name: 'Ci::Build'
has_many :pipelines, class_name: 'Ci::Pipeline'
- has_many :todos
+ has_many :todos, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :authored_todos, class_name: 'Todo', dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
has_many :notification_settings
has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :triggers, class_name: 'Ci::Trigger', foreign_key: :owner_id
has_many :issue_assignees, inverse_of: :assignee
- has_many :merge_request_assignees, inverse_of: :assignee
- has_many :merge_request_reviewers, inverse_of: :reviewer
+ has_many :merge_request_assignees, inverse_of: :assignee, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :merge_request_reviewers, inverse_of: :reviewer, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, class_name: "MergeRequest", through: :merge_request_assignees, source: :merge_request
has_many :created_custom_emoji, class_name: 'CustomEmoji', inverse_of: :creator
@@ -223,7 +233,6 @@ class User < ApplicationRecord
has_many :callouts, class_name: 'Users::Callout'
has_many :group_callouts, class_name: 'Users::GroupCallout'
has_many :project_callouts, class_name: 'Users::ProjectCallout'
- has_many :namespace_callouts, class_name: 'Users::NamespaceCallout'
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
@@ -235,6 +244,7 @@ class User < ApplicationRecord
has_one :user_highest_role
has_one :user_canonical_email
has_one :credit_card_validation, class_name: '::Users::CreditCardValidation'
+ has_one :phone_number_validation, class_name: '::Users::PhoneNumberValidation'
has_one :atlassian_identity, class_name: 'Atlassian::Identity'
has_one :banned_user, class_name: '::Users::BannedUser'
@@ -245,6 +255,8 @@ class User < ApplicationRecord
has_many :timelogs
has_many :resource_label_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
+ has_many :resource_state_events, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
+ has_many :authored_events, class_name: 'Event', dependent: :destroy, foreign_key: :author_id # rubocop:disable Cop/ActiveRecordDependent
#
# Validations
@@ -274,10 +286,10 @@ class User < ApplicationRecord
validate :check_username_format, if: :username_changed?
validates :theme_id, allow_nil: true, inclusion: { in: Gitlab::Themes.valid_ids,
- message: _("%{placeholder} is not a valid theme") % { placeholder: '%{value}' } }
+ message: ->(*) { _("%{placeholder} is not a valid theme") % { placeholder: '%{value}' } } }
validates :color_scheme_id, allow_nil: true, inclusion: { in: Gitlab::ColorSchemes.valid_ids,
- message: _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } }
+ message: ->(*) { _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } } }
validates :website_url, allow_blank: true, url: true, if: :website_url_changed?
@@ -289,6 +301,7 @@ class User < ApplicationRecord
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
before_validation :ensure_namespace_correct
before_save :ensure_namespace_correct # in case validation is skipped
+ before_save :ensure_user_detail_assigned
after_validation :set_username_errors
after_update :username_changed_hook, if: :saved_change_to_username?
after_destroy :post_destroy_hook
@@ -338,8 +351,10 @@ class User < ApplicationRecord
:setup_for_company, :setup_for_company=,
:render_whitespace_in_code, :render_whitespace_in_code=,
:markdown_surround_selection, :markdown_surround_selection=,
+ :markdown_automatic_lists, :markdown_automatic_lists=,
:diffs_deletion_color, :diffs_deletion_color=,
:diffs_addition_color, :diffs_addition_color=,
+ :use_legacy_web_ide, :use_legacy_web_ide=,
to: :user_preference
delegate :path, to: :namespace, allow_nil: true, prefix: true
@@ -934,6 +949,7 @@ class User < ApplicationRecord
# that the password is the user's password
def valid_password?(password)
return false unless password_allowed?(password)
+ return false if password_automatically_set?
return super if Feature.enabled?(:pbkdf2_password_encryption)
Devise::Encryptor.compare(self.class, encrypted_password, password)
@@ -943,6 +959,22 @@ class User < ApplicationRecord
false
end
+ def generate_otp_backup_codes!
+ if Gitlab::FIPS.enabled?
+ generate_otp_backup_codes_pbkdf2!
+ else
+ super
+ end
+ end
+
+ def invalidate_otp_backup_code!(code)
+ if Gitlab::FIPS.enabled? && pbkdf2?
+ invalidate_otp_backup_code_pdkdf2!(code)
+ else
+ super(code)
+ end
+ end
+
# This method should be removed once the :pbkdf2_password_encryption feature flag is removed.
def password=(new_password)
if Feature.enabled?(:pbkdf2_password_encryption) && Feature.enabled?(:pbkdf2_password_encryption_write, self)
@@ -1129,12 +1161,6 @@ class User < ApplicationRecord
end
# rubocop: enable CodeReuse/ServiceClass
- def remove_project_authorizations(project_ids, per_batch = 1000)
- project_ids.each_slice(per_batch) do |project_ids_batch|
- project_authorizations.where(project_id: project_ids_batch).delete_all
- end
- end
-
def authorized_projects(min_access_level = nil)
# We're overriding an association, so explicitly call super with no
# arguments or it would be passed as `force_reload` to the association
@@ -1565,6 +1591,11 @@ class User < ApplicationRecord
end
end
+ # Temporary, will be removed when user_detail fields are fully migrated
+ def ensure_user_detail_assigned
+ user_detail.assign_changed_fields_from_user if UserDetail.user_fields_changed?(self)
+ end
+
def set_username_errors
namespace_path_errors = self.errors.delete(:"namespace.path")
@@ -1647,8 +1678,9 @@ class User < ApplicationRecord
begin
followee = Users::UserFollowUser.create(follower_id: self.id, followee_id: user.id)
self.followees.reset if followee.persisted?
+ followee
rescue ActiveRecord::RecordNotUnique
- false
+ nil
end
end
@@ -1737,7 +1769,7 @@ class User < ApplicationRecord
end
def authorized_project_mirrors(level)
- projects = Ci::ProjectMirror.by_project_id(ci_project_mirrors_for_project_members(level))
+ projects = Ci::ProjectMirror.by_project_id(ci_project_ids_for_project_members(level))
namespace_projects = Ci::ProjectMirror.by_namespace_id(ci_namespace_mirrors_for_group_members(level).select(:namespace_id))
@@ -2075,14 +2107,6 @@ class User < ApplicationRecord
callout_dismissed?(callout, ignore_dismissal_earlier_than)
end
- # Deprecated: do not use. See: https://gitlab.com/gitlab-org/gitlab/-/issues/371017
- def dismissed_callout_for_namespace?(feature_name:, namespace:, ignore_dismissal_earlier_than: nil)
- source_feature_name = "#{feature_name}_#{namespace.id}"
- callout = namespace_callouts_by_feature_name[source_feature_name]
-
- callout_dismissed?(callout, ignore_dismissal_earlier_than)
- end
-
def dismissed_callout_for_project?(feature_name:, project:, ignore_dismissal_earlier_than: nil)
callout = project_callouts.find_by(feature_name: feature_name, project: project)
@@ -2115,11 +2139,6 @@ class User < ApplicationRecord
.find_or_initialize_by(feature_name: ::Users::GroupCallout.feature_names[feature_name], group_id: group_id)
end
- def find_or_initialize_namespace_callout(feature_name, namespace_id)
- namespace_callouts
- .find_or_initialize_by(feature_name: ::Users::NamespaceCallout.feature_names[feature_name], namespace_id: namespace_id)
- end
-
def find_or_initialize_project_callout(feature_name, project_id)
project_callouts
.find_or_initialize_by(feature_name: ::Users::ProjectCallout.feature_names[feature_name], project_id: project_id)
@@ -2198,6 +2217,12 @@ class User < ApplicationRecord
private
+ def pbkdf2?
+ return false unless otp_backup_codes&.any?
+
+ otp_backup_codes.first.start_with?("$pbkdf2-sha512$")
+ end
+
# To enable JiHu repository to modify the default language options
def default_preferred_language
'en'
@@ -2209,7 +2234,7 @@ class User < ApplicationRecord
end
# rubocop: enable CodeReuse/ServiceClass
- def ci_project_mirrors_for_project_members(level)
+ def ci_project_ids_for_project_members(level)
project_members.where('access_level >= ?', level).pluck(:source_id)
end
@@ -2246,10 +2271,6 @@ class User < ApplicationRecord
@group_callouts_by_feature_name ||= group_callouts.index_by(&:source_feature_name)
end
- def namespace_callouts_by_feature_name
- @namespace_callouts_by_feature_name ||= namespace_callouts.index_by(&:source_feature_name)
- end
-
def authorized_groups_without_shared_membership
Group.from_union(
[
@@ -2298,7 +2319,7 @@ class User < ApplicationRecord
self.projects_limit = 0
else
# Only revert these back to the default if they weren't specifically changed in this update.
- self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed?
+ self.can_create_group = Gitlab::CurrentSettings.can_create_group unless can_create_group_changed?
self.projects_limit = Gitlab::CurrentSettings.default_projects_limit unless projects_limit_changed?
end
end
@@ -2363,7 +2384,7 @@ class User < ApplicationRecord
end
def ci_owned_project_runners_from_project_members
- project_ids = ci_project_mirrors_for_project_members(Gitlab::Access::MAINTAINER)
+ project_ids = ci_project_ids_for_project_members(Gitlab::Access::MAINTAINER)
Ci::Runner
.joins(:runner_projects)
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index b9b69d12729..2e662faea6a 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -2,9 +2,6 @@
class UserDetail < ApplicationRecord
extend ::Gitlab::Utils::Override
- include IgnorableColumns
-
- ignore_columns :other_role, remove_after: '2022-07-22', remove_with: '15.3'
REGISTRATION_OBJECTIVE_PAIRS = { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5, joining_team: 6 }.freeze
@@ -15,15 +12,55 @@ class UserDetail < ApplicationRecord
validates :job_title, length: { maximum: 200 }
validates :bio, length: { maximum: 255 }, allow_blank: true
+ DEFAULT_FIELD_LENGTH = 500
+
+ validates :linkedin, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
+ validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true
+
+ before_validation :sanitize_attrs
before_save :prevent_nil_bio
enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
+ def self.user_fields_changed?(user)
+ (%w[linkedin skype twitter website_url location organization] & user.changed).any?
+ end
+
+ def sanitize_attrs
+ %i[linkedin skype twitter website_url].each do |attr|
+ value = self[attr]
+ self[attr] = Sanitize.clean(value) if value.present?
+ end
+ %i[location organization].each do |attr|
+ value = self[attr]
+ self[attr] = Sanitize.clean(value).gsub('&amp;', '&') if value.present?
+ end
+ end
+
+ def assign_changed_fields_from_user
+ self.linkedin = trim_field(user.linkedin) if user.linkedin_changed?
+ self.twitter = trim_field(user.twitter) if user.twitter_changed?
+ self.skype = trim_field(user.skype) if user.skype_changed?
+ self.website_url = trim_field(user.website_url) if user.website_url_changed?
+ self.location = trim_field(user.location) if user.location_changed?
+ self.organization = trim_field(user.organization) if user.organization_changed?
+ end
+
private
def prevent_nil_bio
self.bio = '' if bio_changed? && bio.nil?
end
+
+ def trim_field(value)
+ return '' unless value
+
+ value.first(DEFAULT_FIELD_LENGTH)
+ end
end
UserDetail.prepend_mod_with('UserDetail')
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 9b4c0a2527a..c6ebd550daf 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -22,6 +22,7 @@ class UserPreference < ApplicationRecord
validates :diffs_deletion_color, :diffs_addition_color,
format: { with: ColorsHelper::HEX_COLOR_PATTERN },
allow_blank: true
+ validates :use_legacy_web_ide, allow_nil: false, inclusion: { in: [true, false] }
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
@@ -29,7 +30,6 @@ class UserPreference < ApplicationRecord
default_value_for :time_display_relative, value: true, allows_nil: false
default_value_for :time_format_in_24h, value: false, allows_nil: false
default_value_for :render_whitespace_in_code, value: false, allows_nil: false
- default_value_for :markdown_surround_selection, value: true, allows_nil: false
class << self
def notes_filters
diff --git a/app/models/users/banned_user.rb b/app/models/users/banned_user.rb
index c52b6d4b728..615668e2b55 100644
--- a/app/models/users/banned_user.rb
+++ b/app/models/users/banned_user.rb
@@ -7,6 +7,6 @@ module Users
belongs_to :user
validates :user, presence: true
- validates :user_id, uniqueness: { message: _("banned user already exists") }
+ validates :user_id, uniqueness: { message: N_("banned user already exists") }
end
end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 03841ee48fa..ae6950d800c 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -61,7 +61,8 @@ module Users
namespace_storage_limit_banner_alert_threshold: 57, # EE-only
namespace_storage_limit_banner_error_threshold: 58, # EE-only
project_quality_summary_feedback: 59, # EE-only
- merge_request_settings_moved_callout: 60
+ merge_request_settings_moved_callout: 60,
+ new_top_level_group_alert: 61
}
validates :feature_name,
diff --git a/app/models/users/namespace_callout.rb b/app/models/users/namespace_callout.rb
deleted file mode 100644
index 4e655a96b57..00000000000
--- a/app/models/users/namespace_callout.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Users
- class NamespaceCallout < ApplicationRecord
- include Users::Calloutable
-
- self.table_name = 'user_namespace_callouts'
-
- belongs_to :namespace
-
- enum feature_name: {
- invite_members_banner: 1,
- approaching_seat_count_threshold: 2, # EE-only
- storage_enforcement_banner_first_enforcement_threshold: 3, # EE-only
- storage_enforcement_banner_second_enforcement_threshold: 4, # EE-only
- storage_enforcement_banner_third_enforcement_threshold: 5, # EE-only
- storage_enforcement_banner_fourth_enforcement_threshold: 6, # EE-only
- preview_user_over_limit_free_plan_alert: 7, # EE-only
- user_reached_limit_free_plan_alert: 8, # EE-only
- web_hook_disabled: 9
- }
-
- validates :namespace, presence: true
- validates :feature_name,
- presence: true,
- uniqueness: { scope: [:user_id, :namespace_id] },
- inclusion: { in: NamespaceCallout.feature_names.keys }
-
- def source_feature_name
- "#{feature_name}_#{namespace_id}"
- end
- end
-end
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
new file mode 100644
index 00000000000..f6123c01fd0
--- /dev/null
+++ b/app/models/users/phone_number_validation.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Users
+ class PhoneNumberValidation < ApplicationRecord
+ self.primary_key = :user_id
+ self.table_name = 'user_phone_number_validations'
+
+ belongs_to :user, foreign_key: :user_id
+ belongs_to :banned_user, class_name: '::Users::BannedUser', foreign_key: :user_id
+
+ validates :country,
+ presence: true,
+ length: { maximum: 3 }
+
+ validates :international_dial_code,
+ presence: true,
+ numericality: {
+ only_integer: true,
+ greater_than_or_equal_to: 1,
+ less_than_or_equal_to: 999
+ }
+
+ validates :phone_number,
+ presence: true,
+ format: {
+ with: /\A\d+\Z/,
+ message: -> (object, data) { _('can contain only digits') }
+ },
+ length: { maximum: 12 }
+
+ validates :telesign_reference_xid,
+ length: { maximum: 255 }
+
+ def self.related_to_banned_user?(international_dial_code, phone_number)
+ joins(:banned_user).where(
+ international_dial_code: international_dial_code,
+ phone_number: phone_number
+ ).exists?
+ end
+ end
+end
diff --git a/app/models/users/project_callout.rb b/app/models/users/project_callout.rb
index 98dacbe394a..c73b3a4ee71 100644
--- a/app/models/users/project_callout.rb
+++ b/app/models/users/project_callout.rb
@@ -11,7 +11,11 @@ module Users
enum feature_name: {
awaiting_members_banner: 1, # EE-only
web_hook_disabled: 2,
- ultimate_feature_removal_banner: 3
+ ultimate_feature_removal_banner: 3,
+ storage_enforcement_banner_first_enforcement_threshold: 4, # EE-only
+ storage_enforcement_banner_second_enforcement_threshold: 5, # EE-only
+ storage_enforcement_banner_third_enforcement_threshold: 6, # EE-only
+ storage_enforcement_banner_fourth_enforcement_threshold: 7 # EE-only
}
validates :project, presence: true
diff --git a/app/models/users/user_follow_user.rb b/app/models/users/user_follow_user.rb
index a94239a746c..5a82a81364a 100644
--- a/app/models/users/user_follow_user.rb
+++ b/app/models/users/user_follow_user.rb
@@ -1,7 +1,22 @@
# frozen_string_literal: true
module Users
class UserFollowUser < ApplicationRecord
+ MAX_FOLLOWEE_LIMIT = 300
+
belongs_to :follower, class_name: 'User'
belongs_to :followee, class_name: 'User'
+
+ validate :max_follow_limit
+
+ private
+
+ def max_follow_limit
+ followee_count = self.class.where(follower_id: follower_id).limit(MAX_FOLLOWEE_LIMIT).count
+ return if followee_count < MAX_FOLLOWEE_LIMIT
+
+ errors.add(:base, format(
+ _("You can't follow more than %{limit} users. To follow more users, unfollow some others."),
+ limit: MAX_FOLLOWEE_LIMIT))
+ end
end
end
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index fac79a8194a..b718c3a096f 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -9,6 +9,8 @@ class Wiki
extend ActiveModel::Naming
+ DuplicatePageError = Class.new(StandardError)
+
MARKUPS = { # rubocop:disable Style/MultilineIfModifier
markdown: {
name: 'Markdown',
@@ -109,11 +111,34 @@ class Wiki
end
def sluggified_title(title)
- title = Gitlab::EncodingHelper.encode_utf8_no_detect(title)
- title = File.expand_path(title, '/')
+ title = Gitlab::EncodingHelper.encode_utf8_no_detect(title.to_s.strip)
+ title = File.absolute_path(title, '/')
title = Pathname.new(title).relative_path_from('/').to_s
title.tr(' ', '-')
end
+
+ def canonicalize_filename(filename)
+ ::File.basename(filename, ::File.extname(filename)).tr('-', ' ')
+ end
+
+ def cname(name, char_white_sub = '-', char_other_sub = '-')
+ name.to_s.gsub(/\s/, char_white_sub).gsub(/[<>+]/, char_other_sub)
+ end
+
+ def preview_slug(title, format)
+ ext = format == :markdown ? "md" : format.to_s
+ name = cname(title) + '.' + ext
+ canonical_name = canonicalize_filename(name)
+
+ path =
+ if name.include?('/')
+ name.sub(%r{/[^/]+$}, '/')
+ else
+ ''
+ end
+
+ path + cname(canonical_name, '-', '-')
+ end
end
def initialize(container, user = nil)
@@ -145,14 +170,6 @@ class Wiki
container.path + '.wiki'
end
- # Returns the Gitlab::Git::Wiki object.
- def wiki
- strong_memoize(:wiki) do
- create_wiki_repository
- Gitlab::Git::Wiki.new(repository.raw)
- end
- end
-
def create_wiki_repository
repository.create_if_not_exists(default_branch)
@@ -173,7 +190,7 @@ class Wiki
end
def empty?
- !repository_exists? || list_pages(limit: 1).empty?
+ !repository_exists? || list_page_paths.empty?
end
def exists?
@@ -190,15 +207,9 @@ class Wiki
#
# Returns an Array of GitLab WikiPage instances or an
# empty Array if this Wiki has no pages.
- def list_pages(limit: 0, sort: nil, direction: DIRECTION_ASC, load_content: false)
- wiki.list_pages(
- limit: limit,
- sort: sort,
- direction_desc: direction == DIRECTION_DESC,
- load_content: load_content
- ).map do |page|
- WikiPage.new(self, page)
- end
+ def list_pages(limit: 0, direction: DIRECTION_ASC, load_content: false)
+ create_wiki_repository unless repository_exists?
+ list_pages_with_repository_rpcs(limit: limit, direction: direction, load_content: load_content)
end
def sidebar_entries(limit: Gitlab::WikiPages::MAX_SIDEBAR_PAGES, **options)
@@ -217,19 +228,15 @@ class Wiki
#
# Returns an initialized WikiPage instance or nil
def find_page(title, version = nil, load_content: true)
- if find_page_with_repository_rpcs?
- create_wiki_repository unless repository_exists?
- find_page_with_repository_rpcs(title, version, load_content: load_content)
- else
- find_page_with_legacy_wiki_service(title, version, load_content: load_content)
- end
+ create_wiki_repository unless repository_exists?
+ find_page_with_repository_rpcs(title, version, load_content: load_content)
end
def find_sidebar(version = nil)
find_page(SIDEBAR, version)
end
- def find_file(name, version = 'HEAD', load_content: true)
+ def find_file(name, version = default_branch, load_content: true)
data_limit = load_content ? -1 : 0
blobs = repository.blobs_at([[version, name]], blob_size_limit: data_limit)
@@ -256,7 +263,7 @@ class Wiki
raise_duplicate_page_error!
end
end
- rescue Gitlab::Git::Wiki::DuplicatePageError => e
+ rescue DuplicatePageError => e
@error_message = _("Duplicate page: %{error_message}" % { error_message: e.message })
false
@@ -272,6 +279,7 @@ class Wiki
extension = page.format != format.to_sym ? default_extension : File.extname(page.path).downcase[1..]
capture_git_error(:updated) do
+ create_wiki_repository unless repository_exists?
repository.update_file(
user,
sluggified_full_path(title, extension),
@@ -290,6 +298,7 @@ class Wiki
return unless page
capture_git_error(:deleted) do
+ create_wiki_repository unless repository_exists?
repository.delete_file(user, page.path, **multi_commit_options(:deleted, message, page.title))
after_wiki_activity
@@ -306,8 +315,10 @@ class Wiki
[title, title_array.join("/")]
end
+ # TODO: This method is redundant. Should be replaced by create_wiki_repository
def ensure_repository
- raise CouldNotCreateWikiError unless wiki.repository_exists?
+ create_wiki_repository
+ raise CouldNotCreateWikiError unless repository_exists?
end
def hook_attrs
@@ -343,7 +354,7 @@ class Wiki
override :default_branch
def default_branch
- super || Gitlab::Git::Wiki.default_ref(container)
+ super || Gitlab::DefaultBranch.value(object: container)
end
def wiki_base_path
@@ -423,11 +434,11 @@ class Wiki
escaped_title = Regexp.escape(sluggified_title(title))
regex = Regexp.new("^#{escaped_title}\.#{ALLOWED_EXTENSIONS_REGEX}$", 'i')
- repository.ls_files('HEAD').any? { |s| s =~ regex }
+ repository.ls_files(default_branch).any? { |s| s =~ regex }
end
def raise_duplicate_page_error!
- raise Gitlab::Git::Wiki::DuplicatePageError, _('A page with that title already exists')
+ raise ::Wiki::DuplicatePageError, _('A page with that title already exists')
end
def sluggified_full_path(title, extension)
@@ -439,27 +450,12 @@ class Wiki
end
def canonicalize_filename(filename)
- Gitlab::Git::Wiki::GollumSlug.canonicalize_filename(filename)
- end
-
- def find_page_with_legacy_wiki_service(title, version, load_content: false)
- page_title, page_dir = page_title_and_dir(title)
-
- if page = wiki.page(title: page_title, version: version, dir: page_dir, load_content: load_content)
- WikiPage.new(self, page)
- end
+ self.class.canonicalize_filename(filename)
end
def find_matched_file(title, version)
escaped_path = RE2::Regexp.escape(sluggified_title(title))
- # We could not use ALLOWED_EXTENSIONS_REGEX constant or similar regexp with
- # Regexp.union. The result combination complicated modifiers:
- # /(?i-mx:md|mkdn?|mdown|markdown)|(?i-mx:rdoc).../
- # Regexp used by Gitaly is Go's Regexp package. It does not support those
- # features. So, we have to compose another more-friendly regexp to pass to
- # Gitaly side.
- extension_regexp = Wiki::MARKUPS.map { |_, format| format[:extension_regex].source }.join("|")
- path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)^#{escaped_path}\\.(#{extension_regexp})$")
+ path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)^#{escaped_path}\\.(#{file_extension_regexp})$")
matched_files = repository.search_files_by_regexp(path_regexp, version)
return if matched_files.blank?
@@ -473,11 +469,11 @@ class Wiki
end
def check_page_historical(path, commit)
- repository.last_commit_for_path('HEAD', path).id != commit.id
+ repository.last_commit_for_path(default_branch, path)&.id != commit&.id
end
def find_page_with_repository_rpcs(title, version, load_content: true)
- version = version.presence || 'HEAD'
+ version = version.presence || default_branch
path = find_matched_file(title, version)
return if path.blank?
@@ -487,27 +483,81 @@ class Wiki
format = find_page_format(path)
page = Gitlab::Git::WikiPage.new(
- url_path: sluggified_title(path.sub(/\.[^.]+\z/, "")),
+ url_path: sluggified_title(strip_extension(path)),
title: canonicalize_filename(path),
format: format,
path: sluggified_title(path),
raw_data: blob.data,
name: canonicalize_filename(path),
- historical: version == 'HEAD' ? false : check_page_historical(path, commit),
+ historical: version == default_branch ? false : check_page_historical(path, commit),
version: Gitlab::Git::WikiPageVersion.new(commit, format)
)
WikiPage.new(self, page)
end
- def find_page_with_repository_rpcs?
- group =
- if container.is_a?(::Group)
- container
- else
- container.group
- end
+ def file_extension_regexp
+ # We could not use ALLOWED_EXTENSIONS_REGEX constant or similar regexp with
+ # Regexp.union. The result combination complicated modifiers:
+ # /(?i-mx:md|mkdn?|mdown|markdown)|(?i-mx:rdoc).../
+ # Regexp used by Gitaly is Go's Regexp package. It does not support those
+ # features. So, we have to compose another more-friendly regexp to pass to
+ # Gitaly side.
+ Wiki::MARKUPS.map { |_, format| format[:extension_regex].source }.join("|")
+ end
+
+ def strip_extension(path)
+ path.sub(/\.[^.]+\z/, "")
+ end
- Feature.enabled?(:wiki_find_page_with_normal_repository_rpcs, group, type: :development)
+ def list_page_paths
+ return [] if repository.empty?
+
+ path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)\\.(#{file_extension_regexp})$")
+ repository.search_files_by_regexp(path_regexp, default_branch)
+ end
+
+ def list_pages_with_repository_rpcs(limit:, direction:, load_content:)
+ paths = list_page_paths
+ return [] if paths.empty?
+
+ pages = paths.map do |path|
+ page = Gitlab::Git::WikiPage.new(
+ url_path: sluggified_title(strip_extension(path)),
+ title: canonicalize_filename(path),
+ format: find_page_format(path),
+ path: sluggified_title(path),
+ raw_data: '',
+ name: canonicalize_filename(path),
+ historical: false
+ )
+ WikiPage.new(self, page)
+ end
+ sort_pages!(pages, direction)
+ pages = pages.take(limit) if limit > 0
+ fetch_pages_content!(pages) if load_content
+
+ pages
+ end
+
+ # After migrating to normal repository RPCs, it's very expensive to sort the
+ # pages by created_at. We have to either ListLastCommitsForTree RPC call or
+ # N+1 LastCommitForPath. Either are efficient for a large repository.
+ # Therefore, we decide to sort the title only.
+ def sort_pages!(pages, direction)
+ # Sort by path to ensure the files inside a sub-folder are grouped and sorted together
+ pages.sort_by!(&:path)
+ pages.reverse! if direction == DIRECTION_DESC
+ end
+
+ def fetch_pages_content!(pages)
+ blobs =
+ repository
+ .blobs_at(pages.map { |page| [default_branch, page.path] } )
+ .to_h { |blob| [blob.path, blob.data] }
+
+ pages.each do |page|
+ page.raw_content = blobs[page.path]
+ end
end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 63c60f5a89e..24b0b94eeb7 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -73,7 +73,7 @@ class WikiPage
# The escaped URL path of this page.
def slug
- attributes[:slug].presence || wiki.wiki.preview_slug(title, format)
+ attributes[:slug].presence || ::Wiki.preview_slug(title, format)
end
alias_method :id, :slug # required to use build_stubbed
@@ -99,6 +99,13 @@ class WikiPage
attributes[:content] ||= page&.text_data
end
+ def raw_content=(content)
+ return if page.nil?
+
+ page.raw_data = content
+ attributes[:content] = page.text_data
+ end
+
# The hierarchy of the directory this page is contained in.
def directory
wiki.page_title_and_dir(slug)&.last.to_s
@@ -118,7 +125,7 @@ class WikiPage
def version
return unless persisted?
- @version ||= @page.version
+ @version ||= @page.version || last_version
end
def path
@@ -138,7 +145,7 @@ class WikiPage
default_per_page = Kaminari.config.default_per_page
offset = [options[:page].to_i - 1, 0].max * options.fetch(:per_page, default_per_page)
- wiki.repository.commits('HEAD',
+ wiki.repository.commits(wiki.default_branch,
path: page.path,
limit: options.fetch(:limit, default_per_page),
offset: offset)
@@ -147,11 +154,11 @@ class WikiPage
def count_versions
return [] unless persisted?
- wiki.wiki.count_page_versions(page.path)
+ wiki.repository.count_commits(ref: wiki.default_branch, path: page.path)
end
def last_version
- @last_version ||= versions(limit: 1).first
+ @last_version ||= wiki.repository.last_commit_for_path(wiki.default_branch, page.path) if page
end
def last_commit_sha
diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb
index a52dac446ea..1c23b367489 100644
--- a/app/policies/ci/runner_policy.rb
+++ b/app/policies/ci/runner_policy.rb
@@ -20,8 +20,8 @@ module Ci
end
with_options scope: :user, score: 5
- condition(:any_developer_groups_inheriting_shared_runners) do
- @user.developer_groups.with_shared_runners_enabled.any?
+ condition(:any_developer_maintainer_owned_groups_inheriting_shared_runners) do
+ @user.developer_maintainer_owned_groups.with_shared_runners_enabled.any?
end
with_options scope: :user, score: 5
@@ -31,7 +31,7 @@ module Ci
with_options score: 10
condition(:any_associated_projects_in_group_runner_inheriting_group_runners) do
- # Check if any projects where user is a developer are inheriting group runners
+ # Check if any projects where user is a developer+ are inheriting group runners
@subject.groups&.any? do |group|
group.all_projects
.with_group_runners_enabled
@@ -48,13 +48,10 @@ module Ci
rule { admin | owned_runner }.policy do
enable :read_builds
- end
-
- rule { admin | owned_runner }.policy do
enable :read_runner
end
- rule { is_instance_runner & any_developer_groups_inheriting_shared_runners }.policy do
+ rule { is_instance_runner & any_developer_maintainer_owned_groups_inheriting_shared_runners }.policy do
enable :read_runner
end
diff --git a/app/policies/group_label_policy.rb b/app/policies/group_label_policy.rb
index 9f3acd44b23..4a848e44fec 100644
--- a/app/policies/group_label_policy.rb
+++ b/app/policies/group_label_policy.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class GroupLabelPolicy < BasePolicy
- delegate { @subject.group }
+ delegate { @subject.parent_container }
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 96da0518dc0..7a0fb10928a 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -35,15 +35,15 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
with_options scope: :subject, score: 0
condition(:request_access_enabled) { @subject.request_access_enabled }
- condition(:create_projects_disabled) do
+ condition(:create_projects_disabled, scope: :subject) do
@subject.project_creation_level == ::Gitlab::Access::NO_ONE_PROJECT_ACCESS
end
- condition(:developer_maintainer_access) do
+ condition(:developer_maintainer_access, scope: :subject) do
@subject.project_creation_level == ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS
end
- condition(:maintainer_can_create_group) do
+ condition(:maintainer_can_create_group, scope: :subject) do
@subject.subgroup_creation_level == ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS
end
@@ -51,7 +51,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
group_projects_for(user: @user, group: @subject, only_owned: false).any? { |p| p.design_management_enabled? }
end
- condition(:dependency_proxy_available) do
+ condition(:dependency_proxy_available, scope: :subject) do
@subject.dependency_proxy_feature_available?
end
@@ -59,7 +59,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
access_level(for_any_session: true) >= GroupMember::GUEST || valid_dependency_proxy_deploy_token
end
- condition(:observability_enabled) do
+ condition(:observability_enabled, scope: :subject) do
Feature.enabled?(:observability_group_tab, @subject)
end
@@ -80,10 +80,11 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
with_scope :subject
condition(:has_project_with_service_desk_enabled) { @subject.has_project_with_service_desk_enabled? }
+ with_scope :subject
condition(:crm_enabled, score: 0, scope: :subject) { @subject.crm_enabled? }
- condition(:group_runner_registration_allowed) do
- Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?('group')
+ condition(:group_runner_registration_allowed, scope: :global) do
+ Gitlab::CurrentSettings.valid_runner_registrars.include?('group')
end
rule { can?(:read_group) & design_management_enabled }.policy do
@@ -149,6 +150,8 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :admin_crm_organization
enable :admin_crm_contact
enable :read_cluster
+
+ enable :read_group_all_available_runners
end
rule { reporter }.policy do
@@ -204,6 +207,9 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :destroy_deploy_token
enable :update_runners_registration_token
enable :owner_access
+
+ enable :read_billing
+ enable :edit_billing
end
rule { can?(:read_nested_project_resources) }.policy do
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index e864ce8752a..df065b24e64 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -22,12 +22,6 @@ class IssuablePolicy < BasePolicy
enable :reopen_issue
end
- # This rule replicates permissions in NotePolicy#can_read_confidential and it's used in
- # TodoPolicy for performance reasons
- rule { can?(:reporter_access) | assignee_or_author | admin }.policy do
- enable :read_confidential_notes
- end
-
rule { can?(:read_merge_request) & assignee_or_author }.policy do
enable :update_merge_request
enable :reopen_merge_request
@@ -58,6 +52,12 @@ class IssuablePolicy < BasePolicy
rule { can_read_issuable }.policy do
enable :read_issuable
+ enable :read_issuable_participables
+ end
+
+ # This rule replicates permissions in NotePolicy#can_read_confidential
+ rule { can?(:reporter_access) | assignee_or_author | admin }.policy do
+ enable :read_internal_note
end
end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 0a0a35d41cc..87db228a698 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -81,6 +81,10 @@ class IssuePolicy < IssuablePolicy
rule { can?(:set_issue_metadata) & can_read_crm_contacts }.policy do
enable :set_issue_crm_contacts
end
+
+ rule { can?(:reporter_access) }.policy do
+ enable :mark_note_as_confidential
+ end
end
IssuePolicy.prepend_mod_with('IssuePolicy')
diff --git a/app/policies/namespaces/user_namespace_policy.rb b/app/policies/namespaces/user_namespace_policy.rb
index 028247497e5..89158578ac1 100644
--- a/app/policies/namespaces/user_namespace_policy.rb
+++ b/app/policies/namespaces/user_namespace_policy.rb
@@ -15,6 +15,8 @@ module Namespaces
enable :read_statistics
enable :create_jira_connect_subscription
enable :admin_package
+ enable :read_billing
+ enable :edit_billing
end
rule { ~can_create_personal_project }.prevent :create_projects
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index 1bffcc5aea2..dbfc63a0069 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -20,7 +20,8 @@ class NotePolicy < BasePolicy
condition(:confidential, scope: :subject) { @subject.confidential? }
- # If this condition changes IssuablePolicy#read_confidential_notes should be updated too
+ # Should be matched with IssuablePolicy#read_internal_note
+ # and EpicPolicy#read_internal_note
condition(:can_read_confidential) do
access_level >= Gitlab::Access::REPORTER || @subject.noteable_assignee_or_author?(@user) || admin?
end
diff --git a/app/policies/project_label_policy.rb b/app/policies/project_label_policy.rb
index 5ce896ecaf2..6656d5990a5 100644
--- a/app/policies/project_label_policy.rb
+++ b/app/policies/project_label_policy.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class ProjectLabelPolicy < BasePolicy
- delegate { @subject.project }
+ delegate { @subject.parent_container }
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index fb162d03955..77bdf9d62fc 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -222,7 +222,7 @@ class ProjectPolicy < BasePolicy
end
condition(:project_runner_registration_allowed) do
- Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?('project')
+ Gitlab::CurrentSettings.valid_runner_registrars.include?('project')
end
condition :registry_enabled do
@@ -399,7 +399,7 @@ class ProjectPolicy < BasePolicy
prevent(:admin_feature_flags_client)
end
- rule { split_operations_visibility_permissions & releases_disabled }.policy do
+ rule { releases_disabled }.policy do
prevent(*create_read_update_admin_destroy(:release))
end
@@ -574,6 +574,7 @@ class ProjectPolicy < BasePolicy
rule { issues_disabled & merge_requests_disabled }.policy do
prevent(*create_read_update_admin_destroy(:label))
prevent(*create_read_update_admin_destroy(:milestone))
+ prevent(:read_cycle_analytics)
end
rule { snippets_disabled }.policy do
@@ -793,7 +794,7 @@ class ProjectPolicy < BasePolicy
rule { project_bot }.enable :project_bot_access
- rule { can?(:read_all_resources) }.enable :read_resource_access_tokens
+ rule { can?(:read_all_resources) & resource_access_token_feature_available }.enable :read_resource_access_tokens
rule { can?(:admin_project) & resource_access_token_feature_available }.policy do
enable :read_resource_access_tokens
diff --git a/app/policies/todo_policy.rb b/app/policies/todo_policy.rb
index 5c24964f24a..d63eb9407f8 100644
--- a/app/policies/todo_policy.rb
+++ b/app/policies/todo_policy.rb
@@ -16,7 +16,7 @@ class TodoPolicy < BasePolicy
desc "User can read the todo's confidential note"
condition(:can_read_todo_confidential_note) do
- @user && @user.can?(:read_confidential_notes, @subject.target)
+ @user && @user.can?(:read_internal_note, @subject.target)
end
rule { own_todo & can_read_target }.enable :read_todo
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index 71a05ef2c72..706608e3029 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -34,7 +34,9 @@ module Ci
def runner_variables
stop_expanding_file_vars = ::Feature.enabled?(:ci_stop_expanding_file_vars_for_runners, project)
- variables.sort_and_expand_all(keep_undefined: true, expand_file_vars: !stop_expanding_file_vars).to_runner_variables
+ variables
+ .sort_and_expand_all(keep_undefined: true, expand_file_vars: !stop_expanding_file_vars, project: project)
+ .to_runner_variables
end
def refspecs
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 32a7d205f46..fed4ae7837b 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -92,7 +92,7 @@ module Ci
if all_related_merge_requests.none?
_("No related merge requests found.")
else
- _("%{count} related %{pluralized_subject}: %{links}" % {
+ (_("%{count} related %{pluralized_subject}: %{links}") % {
count: all_related_merge_requests.count,
pluralized_subject: n_('merge request', 'merge requests', all_related_merge_requests.count),
links: all_related_merge_request_links(limit: limit).join(', ')
diff --git a/app/presenters/deploy_key_presenter.rb b/app/presenters/deploy_key_presenter.rb
new file mode 100644
index 00000000000..6f32487b6f6
--- /dev/null
+++ b/app/presenters/deploy_key_presenter.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class DeployKeyPresenter < KeyPresenter # rubocop:disable Gitlab/NamespacedClass
+ presents ::DeployKey, as: :deploy_key
+
+ def humanized_error_message
+ super(type: :deploy_key)
+ end
+end
diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb
index 7fa87d33c0d..2f2fb1aa3ba 100644
--- a/app/presenters/event_presenter.rb
+++ b/app/presenters/event_presenter.rb
@@ -36,6 +36,8 @@ class EventPresenter < Gitlab::View::Presenter::Delegated
'Design'
elsif wiki_page?
'Wiki Page'
+ elsif issue? || work_item?
+ target.issue_type
elsif target_type.present?
target_type.titleize
else
diff --git a/app/presenters/key_presenter.rb b/app/presenters/key_presenter.rb
new file mode 100644
index 00000000000..e3eb5feedbf
--- /dev/null
+++ b/app/presenters/key_presenter.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class KeyPresenter < Gitlab::View::Presenter::Delegated # rubocop:disable Gitlab/NamespacedClass
+ presents ::Key, as: :key_object
+
+ def humanized_error_message(type: :key)
+ if !key_object.public_key.valid?
+ help_link = help_page_link(_('supported SSH public key.'), 'user/ssh', 'supported-ssh-key-types')
+
+ _('%{type} must be a %{help_link}').html_safe % { type: type.to_s.titleize, help_link: help_link }
+ else
+ key_object.errors.full_messages.join(', ').html_safe
+ end
+ end
+
+ private
+
+ def help_page_link(title, path, anchor)
+ ActionController::Base.helpers.link_to(title, help_page_path(path, anchor: anchor),
+ target: '_blank', rel: 'noopener noreferrer')
+ end
+end
diff --git a/app/serializers/board_serializer.rb b/app/serializers/board_serializer.rb
deleted file mode 100644
index 70a4c9ae282..00000000000
--- a/app/serializers/board_serializer.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-class BoardSerializer < BaseSerializer
- entity BoardSimpleEntity
-end
diff --git a/app/serializers/board_simple_entity.rb b/app/serializers/board_simple_entity.rb
deleted file mode 100644
index ab625490966..00000000000
--- a/app/serializers/board_simple_entity.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-class BoardSimpleEntity < Grape::Entity
- expose :id
- expose :name
-end
-
-BoardSimpleEntity.prepend_mod_with('BoardSimpleEntity')
diff --git a/app/serializers/current_board_entity.rb b/app/serializers/current_board_entity.rb
deleted file mode 100644
index 530f7f5dea3..00000000000
--- a/app/serializers/current_board_entity.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-class CurrentBoardEntity < Grape::Entity
- expose :id
- expose :name
- expose :hide_backlog_list
- expose :hide_closed_list
-end
-
-CurrentBoardEntity.prepend_mod_with('CurrentBoardEntity')
diff --git a/app/serializers/current_board_serializer.rb b/app/serializers/current_board_serializer.rb
deleted file mode 100644
index c58c77194f2..00000000000
--- a/app/serializers/current_board_serializer.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-class CurrentBoardSerializer < BaseSerializer
- entity CurrentBoardEntity
-end
diff --git a/app/serializers/group_access_token_entity.rb b/app/serializers/group_access_token_entity.rb
index ab1fbb8ab46..83e8284e4e2 100644
--- a/app/serializers/group_access_token_entity.rb
+++ b/app/serializers/group_access_token_entity.rb
@@ -11,7 +11,7 @@ class GroupAccessTokenEntity < AccessTokenEntityBase
revoke_group_settings_access_token_path(
id: token,
- group_id: group.path)
+ group_id: group.full_path)
end
expose :role do |token, options|
diff --git a/app/serializers/import/github_org_entity.rb b/app/serializers/import/github_org_entity.rb
new file mode 100644
index 00000000000..a250a9b60f5
--- /dev/null
+++ b/app/serializers/import/github_org_entity.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Import
+ class GithubOrgEntity < Grape::Entity
+ expose :login, as: :name
+ expose :description
+ end
+end
diff --git a/app/serializers/import/github_org_serializer.rb b/app/serializers/import/github_org_serializer.rb
new file mode 100644
index 00000000000..69a598e4b24
--- /dev/null
+++ b/app/serializers/import/github_org_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Import
+ class GithubOrgSerializer < BaseSerializer
+ entity Import::GithubOrgEntity
+ end
+end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 7ff75927fcd..3d94d2e2e9d 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -43,6 +43,10 @@ class IssueEntity < IssuableEntity
can?(request.current_user, :create_note, issue)
end
+ expose :can_create_confidential_note do |issue|
+ can?(request.current_user, :mark_note_as_confidential, issue)
+ end
+
expose :can_update do |issue|
can?(request.current_user, :update_issue, issue)
end
diff --git a/app/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb
index 29bd26c3a15..07d7d19d1f3 100644
--- a/app/serializers/merge_request_noteable_entity.rb
+++ b/app/serializers/merge_request_noteable_entity.rb
@@ -51,7 +51,7 @@ class MergeRequestNoteableEntity < IssuableEntity
end
expose :can_approve do |merge_request|
- merge_request.can_be_approved_by?(current_user)
+ merge_request.eligible_for_approval_by?(current_user)
end
end
diff --git a/app/serializers/merge_request_user_entity.rb b/app/serializers/merge_request_user_entity.rb
index 36825d14062..caf2e4c89b6 100644
--- a/app/serializers/merge_request_user_entity.rb
+++ b/app/serializers/merge_request_user_entity.rb
@@ -25,6 +25,10 @@ class MergeRequestUserEntity < ::API::Entities::UserBasic
# makes one query per merge request, whereas #approved_by? makes one per user
options[:merge_request].approvals.any? { |app| app.user_id == user.id }
end
+
+ expose :suggested, if: satisfies(:present?) do |user, options|
+ options[:suggested]
+ end
end
MergeRequestUserEntity.prepend_mod_with('MergeRequestUserEntity')
diff --git a/app/serializers/project_access_token_entity.rb b/app/serializers/project_access_token_entity.rb
index 52bb7b05d4e..548fb24173a 100644
--- a/app/serializers/project_access_token_entity.rb
+++ b/app/serializers/project_access_token_entity.rb
@@ -11,7 +11,7 @@ class ProjectAccessTokenEntity < AccessTokenEntityBase
revoke_namespace_project_settings_access_token_path(
id: token,
- namespace_id: project.namespace.path,
+ namespace_id: project.namespace.full_path,
project_id: project.path)
end
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index 5e7dab31e8a..5082b84978a 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -8,7 +8,7 @@ class UserSerializer < BaseSerializer
merge_request = opts[:project].merge_requests.find_by_iid!(params[:merge_request_iid])
preload_max_member_access(merge_request.project, Array(resource))
- super(resource, opts.merge(merge_request: merge_request), MergeRequestUserEntity)
+ super(resource, opts.merge(merge_request: merge_request, suggested: params[:suggested]), MergeRequestUserEntity)
else
super
end
diff --git a/app/services/admin/set_feature_flag_service.rb b/app/services/admin/set_feature_flag_service.rb
new file mode 100644
index 00000000000..d72a18a6a58
--- /dev/null
+++ b/app/services/admin/set_feature_flag_service.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module Admin
+ class SetFeatureFlagService
+ def initialize(feature_flag_name:, params:)
+ @name = feature_flag_name
+ @params = params
+ end
+
+ def execute
+ unless params[:force]
+ error = validate_feature_flag_name
+ return ServiceResponse.error(message: error, reason: :invalid_feature_flag) if error
+ end
+
+ flag_target = Feature::Target.new(params)
+ value = gate_value(params)
+
+ case value
+ when true
+ enable!(flag_target)
+ when false
+ disable!(flag_target)
+ else
+ enable_partially!(value, params)
+ end
+
+ feature_flag = Feature.get(name) # rubocop:disable Gitlab/AvoidFeatureGet
+
+ ServiceResponse.success(payload: { feature_flag: feature_flag })
+ rescue Feature::Target::UnknowTargetError => e
+ ServiceResponse.error(message: e.message, reason: :actor_not_found)
+ end
+
+ private
+
+ attr_reader :name, :params
+
+ def enable!(flag_target)
+ if flag_target.gate_specified?
+ flag_target.targets.each { |target| Feature.enable(name, target) }
+ else
+ Feature.enable(name)
+ end
+ end
+
+ def disable!(flag_target)
+ if flag_target.gate_specified?
+ flag_target.targets.each { |target| Feature.disable(name, target) }
+ else
+ Feature.disable(name)
+ end
+ end
+
+ def enable_partially!(value, params)
+ if params[:key] == 'percentage_of_actors'
+ Feature.enable_percentage_of_actors(name, value)
+ else
+ Feature.enable_percentage_of_time(name, value)
+ end
+ end
+
+ def validate_feature_flag_name
+ # overridden in EE
+ end
+
+ def gate_value(params)
+ case params[:value]
+ when 'true'
+ true
+ when '0', 'false'
+ false
+ else
+ # https://github.com/jnunemaker/flipper/blob/master/lib/flipper/typecast.rb#L47
+ if params[:value].to_s.include?('.')
+ params[:value].to_f
+ else
+ params[:value].to_i
+ end
+ end
+ end
+ end
+end
+
+Admin::SetFeatureFlagService.prepend_mod
diff --git a/app/services/alert_management/create_alert_issue_service.rb b/app/services/alert_management/create_alert_issue_service.rb
index 34c2003bd01..28e312a6fa3 100644
--- a/app/services/alert_management/create_alert_issue_service.rb
+++ b/app/services/alert_management/create_alert_issue_service.rb
@@ -21,7 +21,7 @@ module AlertManagement
result = create_incident
return result unless result.success?
- issue = result.payload[:issue]
+ issue = result[:issue]
perform_after_create_tasks(issue)
result
diff --git a/app/services/authorized_project_update/project_recalculate_service.rb b/app/services/authorized_project_update/project_recalculate_service.rb
index e0b8158417c..8d60fffd959 100644
--- a/app/services/authorized_project_update/project_recalculate_service.rb
+++ b/app/services/authorized_project_update/project_recalculate_service.rb
@@ -64,7 +64,12 @@ module AuthorizedProjectUpdate
end
def refresh_authorizations
- project.remove_project_authorizations(user_ids_to_remove) if user_ids_to_remove.any?
+ if user_ids_to_remove.any?
+ ProjectAuthorization.delete_all_in_batches_for_project(
+ project: project,
+ user_ids: user_ids_to_remove)
+ end
+
ProjectAuthorization.insert_all_in_batches(authorizations_to_create) if authorizations_to_create.any?
end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 465025ef2e9..fcaa74555ca 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -50,7 +50,7 @@ module Boards
end
def set_issue_types
- params[:issue_types] ||= Issue::TYPES_FOR_LIST
+ params[:issue_types] ||= Issue::TYPES_FOR_BOARD_LIST
end
def item_model
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
deleted file mode 100644
index d74320e92a3..00000000000
--- a/app/services/boards/lists/generate_service.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-module Boards
- module Lists
- class GenerateService < Boards::BaseService
- def execute(board)
- return false unless board.lists.movable.empty?
-
- List.transaction do
- label_params.each do |params|
- response = create_list(board, params)
-
- raise ActiveRecord::Rollback unless response.success?
- end
- end
-
- true
- end
-
- private
-
- def create_list(board, params)
- label = find_or_create_label(params)
- Lists::CreateService.new(parent, current_user, label_id: label.id).execute(board)
- end
-
- def find_or_create_label(params)
- ::Labels::FindOrCreateService.new(current_user, parent, params).execute
- end
-
- def label_params
- [
- { name: 'To Do', color: '#F0AD4E' },
- { name: 'Doing', color: '#5CB85C' }
- ]
- end
- end
- end
-end
diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb
index e81ef467a4e..cf15db4314c 100644
--- a/app/services/boards/lists/list_service.rb
+++ b/app/services/boards/lists/list_service.rb
@@ -9,23 +9,27 @@ module Boards
end
lists = board.lists.preload_associated_models
+ lists = lists.with_types(available_list_types_for(board))
return lists.id_in(params[:list_id]) if params[:list_id].present?
- list_types = unavailable_list_types_for(board)
- lists.without_types(list_types)
+ lists
end
private
- def unavailable_list_types_for(board)
- hidden_lists_for(board)
+ def available_list_types_for(board)
+ licensed_list_types(board) + visible_lists(board)
end
- def hidden_lists_for(board)
- [].tap do |hidden|
- hidden << ::List.list_types[:backlog] if board.hide_backlog_list?
- hidden << ::List.list_types[:closed] if board.hide_closed_list?
+ def licensed_list_types(board)
+ [List.list_types[:label]]
+ end
+
+ def visible_lists(board)
+ [].tap do |visible|
+ visible << ::List.list_types[:backlog] unless board.hide_backlog_list?
+ visible << ::List.list_types[:closed] unless board.hide_closed_list?
end
end
end
diff --git a/app/services/bulk_imports/create_pipeline_trackers_service.rb b/app/services/bulk_imports/create_pipeline_trackers_service.rb
index af97aec09b5..f5b944e6df5 100644
--- a/app/services/bulk_imports/create_pipeline_trackers_service.rb
+++ b/app/services/bulk_imports/create_pipeline_trackers_service.rb
@@ -53,11 +53,13 @@ module BulkImports
def log_skipped_pipeline(pipeline, minimum_version, maximum_version)
logger.info(
message: 'Pipeline skipped as source instance version not compatible with pipeline',
- entity_id: entity.id,
+ bulk_import_entity_id: entity.id,
+ bulk_import_id: entity.bulk_import_id,
pipeline_name: pipeline[:pipeline],
minimum_source_version: minimum_version,
maximum_source_version: maximum_version,
- source_version: source_version.to_s
+ source_version: source_version.to_s,
+ importer: 'gitlab_migration'
)
end
diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb
index 31e1a822e78..d3c6dcca588 100644
--- a/app/services/bulk_imports/create_service.rb
+++ b/app/services/bulk_imports/create_service.rb
@@ -38,6 +38,8 @@ module BulkImports
def execute
bulk_import = create_bulk_import
+ Gitlab::Tracking.event(self.class.name, 'create', label: 'bulk_import_group')
+
BulkImportWorker.perform_async(bulk_import.id)
ServiceResponse.success(payload: bulk_import)
diff --git a/app/services/bulk_imports/repository_bundle_export_service.rb b/app/services/bulk_imports/repository_bundle_export_service.rb
index 31a2ed6d1af..86159f5189d 100644
--- a/app/services/bulk_imports/repository_bundle_export_service.rb
+++ b/app/services/bulk_imports/repository_bundle_export_service.rb
@@ -9,13 +9,19 @@ module BulkImports
end
def execute
- repository.bundle_to_disk(bundle_filepath) if repository.exists?
+ return unless repository_exists?
+
+ repository.bundle_to_disk(bundle_filepath)
end
private
attr_reader :repository, :export_path, :export_filename
+ def repository_exists?
+ repository.exists? && !repository.empty?
+ end
+
def bundle_filepath
File.join(export_path, "#{export_filename}.bundle")
end
diff --git a/app/services/bulk_imports/uploads_export_service.rb b/app/services/bulk_imports/uploads_export_service.rb
index 7f5ee7b8624..315590bea31 100644
--- a/app/services/bulk_imports/uploads_export_service.rb
+++ b/app/services/bulk_imports/uploads_export_service.rb
@@ -22,8 +22,9 @@ module BulkImports
subdir_path = export_subdir_path(upload)
mkdir_p(subdir_path)
download_or_copy_upload(uploader, File.join(subdir_path, uploader.filename))
- rescue Errno::ENAMETOOLONG => e
- # Do not fail entire export process if downloaded file has filename that exceeds 255 characters.
+ rescue StandardError => e
+ # Do not fail entire project export if something goes wrong during file download
+ # (e.g. downloaded file has filename that exceeds 255 characters).
# Ignore raised exception, skip such upload, log the error and keep going with the export instead.
Gitlab::ErrorTracking.log_exception(e, portable_id: portable.id, portable_class: portable.class.name, upload_id: upload.id)
end
diff --git a/app/services/ci/after_requeue_job_service.rb b/app/services/ci/after_requeue_job_service.rb
index 634c547a623..9d54207d75d 100644
--- a/app/services/ci/after_requeue_job_service.rb
+++ b/app/services/ci/after_requeue_job_service.rb
@@ -26,7 +26,7 @@ module Ci
return legacy_dependent_jobs unless ::Feature.enabled?(:ci_requeue_with_dag_object_hierarchy, project)
ordered_by_dag(
- ::Ci::Processable
+ @processable.pipeline.processables
.from_union(needs_dependent_jobs, stage_dependent_jobs)
.skipped
.ordered_by_stage
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index af175b8da1c..0b49beffcb5 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -26,6 +26,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::AssignPartition,
Gitlab::Ci::Pipeline::Chain::Seed,
Gitlab::Ci::Pipeline::Chain::Limit::Size,
+ Gitlab::Ci::Pipeline::Chain::Limit::ActiveJobs,
Gitlab::Ci::Pipeline::Chain::Limit::Deployments,
Gitlab::Ci::Pipeline::Chain::Validate::External,
Gitlab::Ci::Pipeline::Chain::Populate,
@@ -36,7 +37,6 @@ module Ci
Gitlab::Ci::Pipeline::Chain::CreateDeployments,
Gitlab::Ci::Pipeline::Chain::CreateCrossDatabaseAssociations,
Gitlab::Ci::Pipeline::Chain::Limit::Activity,
- Gitlab::Ci::Pipeline::Chain::Limit::JobActivity,
Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines,
Gitlab::Ci::Pipeline::Chain::Metrics,
Gitlab::Ci::Pipeline::Chain::TemplateUsage,
@@ -140,7 +140,7 @@ module Ci
end
def create_namespace_onboarding_action
- Namespaces::OnboardingPipelineCreatedWorker.perform_async(project.namespace_id)
+ Onboarding::PipelineCreatedWorker.perform_async(project.namespace_id)
end
def extra_options(content: nil, dry_run: false)
diff --git a/app/services/ci/generate_kubeconfig_service.rb b/app/services/ci/generate_kubeconfig_service.rb
index 894ab8e8505..347bc99dbf5 100644
--- a/app/services/ci/generate_kubeconfig_service.rb
+++ b/app/services/ci/generate_kubeconfig_service.rb
@@ -14,7 +14,8 @@ module Ci
url: Gitlab::Kas.tunnel_url
)
- agents.each do |agent|
+ agent_authorizations.each do |authorization|
+ agent = authorization.agent
user = user_name(agent)
template.add_user(
@@ -24,6 +25,7 @@ module Ci
template.add_context(
name: context_name(agent),
+ namespace: context_namespace(authorization),
cluster: cluster_name,
user: user
)
@@ -36,8 +38,8 @@ module Ci
attr_reader :pipeline, :token, :template
- def agents
- pipeline.authorized_cluster_agents
+ def agent_authorizations
+ pipeline.cluster_agent_authorizations
end
def cluster_name
@@ -52,6 +54,10 @@ module Ci
[agent.project.full_path, agent.name].join(delimiter)
end
+ def context_namespace(authorization)
+ authorization.config['default_namespace']
+ end
+
def agent_token(agent)
['ci', agent.id, token].join(delimiter)
end
diff --git a/app/services/ci/job_artifacts/delete_service.rb b/app/services/ci/job_artifacts/delete_service.rb
index 65cae03312e..c9d590eccc4 100644
--- a/app/services/ci/job_artifacts/delete_service.rb
+++ b/app/services/ci/job_artifacts/delete_service.rb
@@ -15,13 +15,23 @@ module Ci
method: 'Ci::JobArtifacts::DeleteService#execute',
project_id: build.project_id
)
+ return ServiceResponse.error(
+ message: 'Action temporarily disabled. The project this job belongs to is undergoing stats refresh.',
+ reason: :project_stats_refresh
+ )
end
- # fix_expire_at is false because in this case we want to explicitly delete the job artifacts
- # this flag is a workaround that will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/355833
- Ci::JobArtifacts::DestroyBatchService.new(build.job_artifacts.erasable, fix_expire_at: false).execute
+ result = Ci::JobArtifacts::DestroyBatchService.new(build.job_artifacts.erasable).execute
- ServiceResponse.success
+ if result.fetch(:status) == :success
+ ServiceResponse.success(payload:
+ {
+ destroyed_artifacts_count: result.fetch(:destroyed_artifacts_count),
+ statistics_updates: result.fetch(:statistics_updates)
+ })
+ else
+ ServiceResponse.error(message: result.fetch(:message))
+ end
end
private
diff --git a/app/services/ci/parse_dotenv_artifact_service.rb b/app/services/ci/parse_dotenv_artifact_service.rb
index fd13ed245cf..14e8dc41cf5 100644
--- a/app/services/ci/parse_dotenv_artifact_service.rb
+++ b/app/services/ci/parse_dotenv_artifact_service.rb
@@ -40,7 +40,7 @@ module Ci
key, value = scan_line!(line)
variables[key] = Ci::JobVariable.new(job_id: artifact.job_id,
- source: :dotenv, key: key, value: value)
+ source: :dotenv, key: key, value: value, raw: false)
end
end
diff --git a/app/services/ci/pipeline_artifacts/coverage_report_service.rb b/app/services/ci/pipeline_artifacts/coverage_report_service.rb
index 99877603554..9c6fdb7a405 100644
--- a/app/services/ci/pipeline_artifacts/coverage_report_service.rb
+++ b/app/services/ci/pipeline_artifacts/coverage_report_service.rb
@@ -27,18 +27,13 @@ module Ci
end
def pipeline_artifact_params
- attributes = {
+ {
pipeline: pipeline,
file_type: :code_coverage,
file: carrierwave_file,
- size: carrierwave_file['tempfile'].size
+ size: carrierwave_file['tempfile'].size,
+ locked: pipeline.locked
}
-
- if ::Feature.enabled?(:ci_update_unlocked_pipeline_artifacts, pipeline.project)
- attributes[:locked] = pipeline.locked
- end
-
- attributes
end
def carrierwave_file
diff --git a/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb b/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
index aeb68a75f88..a0746ef32b2 100644
--- a/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
+++ b/app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb
@@ -23,20 +23,15 @@ module Ci
def artifact_attributes
file = build_carrierwave_file!
- attributes = {
+ {
project_id: pipeline.project_id,
file_type: :code_quality_mr_diff,
file_format: Ci::PipelineArtifact::REPORT_TYPES.fetch(:code_quality_mr_diff),
size: file["tempfile"].size,
file: file,
- expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now
+ expire_at: Ci::PipelineArtifact::EXPIRATION_DATE.from_now,
+ locked: pipeline.locked
}
-
- if ::Feature.enabled?(:ci_update_unlocked_pipeline_artifacts, pipeline.project)
- attributes[:locked] = pipeline.locked
- end
-
- attributes
end
def merge_requests
diff --git a/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb b/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb
index 17c039885e5..8dddf3c3f6c 100644
--- a/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb
+++ b/app/services/ci/pipeline_artifacts/destroy_all_expired_service.rb
@@ -3,20 +3,26 @@
module Ci
module PipelineArtifacts
class DestroyAllExpiredService
+ include ::Gitlab::ExclusiveLeaseHelpers
include ::Gitlab::LoopHelpers
include ::Gitlab::Utils::StrongMemoize
BATCH_SIZE = 100
- LOOP_TIMEOUT = 5.minutes
LOOP_LIMIT = 1000
+ LOOP_TIMEOUT = 5.minutes
+ LOCK_TIMEOUT = 10.minutes
+ EXCLUSIVE_LOCK_KEY = 'expired_pipeline_artifacts:destroy:lock'
def initialize
@removed_artifacts_count = 0
+ @start_at = Time.current
end
def execute
- loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
- destroy_artifacts_batch
+ in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
+ destroy_unlocked_pipeline_artifacts
+
+ legacy_destroy_pipeline_artifacts
end
@removed_artifacts_count
@@ -24,10 +30,30 @@ module Ci
private
+ def destroy_unlocked_pipeline_artifacts
+ loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
+ artifacts = Ci::PipelineArtifact.expired_before(@start_at).artifact_unlocked.limit(BATCH_SIZE)
+
+ break if artifacts.empty?
+
+ destroy_batch(artifacts)
+ end
+ end
+
+ def legacy_destroy_pipeline_artifacts
+ loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
+ destroy_artifacts_batch
+ end
+ end
+
def destroy_artifacts_batch
artifacts = ::Ci::PipelineArtifact.unlocked.expired.limit(BATCH_SIZE).to_a
return false if artifacts.empty?
+ destroy_batch(artifacts)
+ end
+
+ def destroy_batch(artifacts)
artifacts.each(&:destroy!)
increment_stats(artifacts.size)
diff --git a/app/services/ci/process_build_service.rb b/app/services/ci/process_build_service.rb
index e6ec65fcc91..22cd267806d 100644
--- a/app/services/ci/process_build_service.rb
+++ b/app/services/ci/process_build_service.rb
@@ -25,6 +25,8 @@ module Ci
end
def enqueue(build)
+ return build.drop!(:failed_outdated_deployment_job) if build.prevent_rollback_deployment?
+
build.enqueue
end
diff --git a/app/services/ci/runners/register_runner_service.rb b/app/services/ci/runners/register_runner_service.rb
index ae9b8bc8a16..abd32610cec 100644
--- a/app/services/ci/runners/register_runner_service.rb
+++ b/app/services/ci/runners/register_runner_service.rb
@@ -59,7 +59,7 @@ module Ci
end
def runner_registrar_valid?(type)
- Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type)
+ Gitlab::CurrentSettings.valid_runner_registrars.include?(type)
end
def token_scope
diff --git a/app/services/ci/unlock_artifacts_service.rb b/app/services/ci/unlock_artifacts_service.rb
index 1fee31da4fc..574cdae6480 100644
--- a/app/services/ci/unlock_artifacts_service.rb
+++ b/app/services/ci/unlock_artifacts_service.rb
@@ -11,8 +11,6 @@ module Ci
unlocked_pipeline_artifacts: 0
}
- unlock_pipeline_artifacts_enabled = ::Feature.enabled?(:ci_update_unlocked_pipeline_artifacts, ci_ref.project)
-
if ::Feature.enabled?(:ci_update_unlocked_job_artifacts, ci_ref.project)
loop do
unlocked_pipelines = []
@@ -22,9 +20,7 @@ module Ci
unlocked_pipelines = unlock_pipelines(ci_ref, before_pipeline)
unlocked_job_artifacts = unlock_job_artifacts(unlocked_pipelines)
- if unlock_pipeline_artifacts_enabled
- results[:unlocked_pipeline_artifacts] += unlock_pipeline_artifacts(unlocked_pipelines)
- end
+ results[:unlocked_pipeline_artifacts] += unlock_pipeline_artifacts(unlocked_pipelines)
end
break if unlocked_pipelines.empty?
diff --git a/app/services/clusters/applications/destroy_service.rb b/app/services/clusters/applications/destroy_service.rb
deleted file mode 100644
index d666682487b..00000000000
--- a/app/services/clusters/applications/destroy_service.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class DestroyService < ::Clusters::Applications::BaseService
- def execute(_request)
- instantiate_application.tap do |application|
- break unless application.can_uninstall?
-
- application.make_scheduled!
-
- Clusters::Applications::UninstallWorker.perform_async(application.name, application.id)
- end
- end
-
- private
-
- def builder
- cluster.public_send(application_class.association_name) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
- end
-end
diff --git a/app/services/clusters/applications/uninstall_service.rb b/app/services/clusters/applications/uninstall_service.rb
deleted file mode 100644
index 50c8d806c14..00000000000
--- a/app/services/clusters/applications/uninstall_service.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class UninstallService < BaseHelmService
- def execute
- return unless app.scheduled?
-
- app.make_uninstalling!
- uninstall
- end
-
- private
-
- def uninstall
- helm_api.uninstall(app.uninstall_command)
-
- Clusters::Applications::WaitForUninstallAppWorker.perform_in(
- Clusters::Applications::WaitForUninstallAppWorker::INTERVAL, app.name, app.id)
- rescue Kubeclient::HttpError => e
- log_error(e)
- app.make_errored!("Kubernetes error: #{e.error_code}")
- rescue StandardError => e
- log_error(e)
- app.make_errored!('Failed to uninstall.')
- end
- end
- end
-end
diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb
index c1c93aa604e..281b2508090 100644
--- a/app/services/concerns/users/participable_service.rb
+++ b/app/services/concerns/users/participable_service.rb
@@ -32,6 +32,8 @@ module Users
end
def groups
+ return [] unless current_user
+
current_user.authorized_groups.with_route.sort_by(&:path)
end
diff --git a/app/services/concerns/work_items/widgetable_service.rb b/app/services/concerns/work_items/widgetable_service.rb
index beb614c7b76..24ade9336b2 100644
--- a/app/services/concerns/work_items/widgetable_service.rb
+++ b/app/services/concerns/work_items/widgetable_service.rb
@@ -2,18 +2,22 @@
module WorkItems
module WidgetableService
- def execute_widgets(work_item:, callback:, widget_params: {})
+ def execute_widgets(work_item:, callback:, widget_params: {}, service_params: {})
work_item.widgets.each do |widget|
- widget_service(widget).try(callback, params: widget_params[widget.class.api_symbol])
+ widget_service(widget, service_params).try(callback, params: widget_params[widget.class.api_symbol])
end
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
- def widget_service(widget)
+ def widget_service(widget, service_params)
@widget_services ||= {}
return @widget_services[widget] if @widget_services.has_key?(widget)
- @widget_services[widget] = widget_service_class(widget)&.new(widget: widget, current_user: current_user)
+ @widget_services[widget] = widget_service_class(widget)&.new(
+ widget: widget,
+ current_user: current_user,
+ service_params: service_params
+ )
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/services/google_cloud/enable_cloudsql_service.rb b/app/services/google_cloud/enable_cloudsql_service.rb
index e4a411d0fab..911cccca5ca 100644
--- a/app/services/google_cloud/enable_cloudsql_service.rb
+++ b/app/services/google_cloud/enable_cloudsql_service.rb
@@ -3,7 +3,7 @@
module GoogleCloud
class EnableCloudsqlService < ::GoogleCloud::BaseService
def execute
- return no_projects_error if unique_gcp_project_ids.empty?
+ create_or_replace_project_vars(environment_name, 'GCP_PROJECT_ID', gcp_project_id, ci_var_protected?)
unique_gcp_project_ids.each do |gcp_project_id|
google_api_client.enable_cloud_sql_admin(gcp_project_id)
@@ -18,8 +18,8 @@ module GoogleCloud
private
- def no_projects_error
- error("No GCP projects found. Configure a service account or GCP_PROJECT_ID CI variable.")
+ def ci_var_protected?
+ ProtectedBranch.protected?(project, environment_name) || ProtectedTag.protected?(project, environment_name)
end
end
end
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index db52a272bf2..4092ded67bc 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -26,6 +26,8 @@ module Groups
end
def execute
+ Gitlab::Tracking.event(self.class.name, 'create', label: 'import_group_from_file')
+
if valid_user_permissions? && import_file && restorers.all?(&:restore)
notify_success
diff --git a/app/services/import/github/cancel_project_import_service.rb b/app/services/import/github/cancel_project_import_service.rb
new file mode 100644
index 00000000000..5dce5e73662
--- /dev/null
+++ b/app/services/import/github/cancel_project_import_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Import
+ module Github
+ class CancelProjectImportService < ::BaseService
+ def execute
+ return error('Not Found', :not_found) unless authorized_to_read?
+ return error('Unauthorized access', :forbidden) unless authorized_to_cancel?
+
+ if project.import_in_progress?
+ project.import_state.cancel
+ success(project: project)
+ else
+ error(cannot_cancel_error_message, :bad_request)
+ end
+ end
+
+ private
+
+ def authorized_to_read?
+ can?(current_user, :read_project, project)
+ end
+
+ def authorized_to_cancel?
+ can?(current_user, :owner_access, project)
+ end
+
+ def cannot_cancel_error_message
+ format(
+ _('The import cannot be canceled because it is %{project_status}'),
+ project_status: project.import_state.status
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
index 53297d2412c..a60963e28c7 100644
--- a/app/services/import/github_service.rb
+++ b/app/services/import/github_service.rb
@@ -9,21 +9,13 @@ module Import
attr_reader :params, :current_user
def execute(access_params, provider)
- if blocked_url?
- return log_and_return_error("Invalid URL: #{url}", _("Invalid URL: %{url}") % { url: url }, :bad_request)
- end
-
- unless authorized?
- return error(_('This namespace has already been taken! Please choose another one.'), :unprocessable_entity)
- end
-
- if oversized?
- return error(oversize_error_message, :unprocessable_entity)
- end
+ context_error = validate_context
+ return context_error if context_error
project = create_project(access_params, provider)
if project.persisted?
+ store_import_settings(project)
success(project)
elsif project.errors[:import_source_disabled].present?
error(project.errors[:import_source_disabled], :forbidden)
@@ -108,6 +100,16 @@ module Import
private
+ def validate_context
+ if blocked_url?
+ log_and_return_error("Invalid URL: #{url}", _("Invalid URL: %{url}") % { url: url }, :bad_request)
+ elsif !authorized?
+ error(_('This namespace has already been taken. Choose a different one.'), :unprocessable_entity)
+ elsif oversized?
+ error(oversize_error_message, :unprocessable_entity)
+ end
+ end
+
def log_error(exception)
Gitlab::GithubImport::Logger.error(
message: 'Import failed due to a GitHub error',
@@ -126,6 +128,10 @@ module Import
error(translated_message, http_status)
end
+
+ def store_import_settings(project)
+ Gitlab::GithubImport::Settings.new(project).write(params[:optional_stages])
+ end
end
end
diff --git a/app/services/incident_management/incidents/create_service.rb b/app/services/incident_management/incidents/create_service.rb
index ef66325fdcc..f44842650b7 100644
--- a/app/services/incident_management/incidents/create_service.rb
+++ b/app/services/incident_management/incidents/create_service.rb
@@ -15,7 +15,7 @@ module IncidentManagement
end
def execute
- issue = Issues::CreateService.new(
+ create_result = Issues::CreateService.new(
project: project,
current_user: current_user,
params: {
@@ -29,22 +29,16 @@ module IncidentManagement
).execute
if alert
- return error(alert.errors.full_messages.to_sentence, issue) unless alert.valid?
+ return error(alert.errors.full_messages, create_result[:issue]) unless alert.valid?
end
- return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?
-
- success(issue)
+ create_result
end
private
attr_reader :title, :description, :severity, :alert
- def success(issue)
- ServiceResponse.success(payload: { issue: issue })
- end
-
def error(message, issue = nil)
ServiceResponse.error(payload: { issue: issue }, message: message)
end
diff --git a/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb b/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb
index 58777848151..d495ec5cab6 100644
--- a/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb
+++ b/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb
@@ -11,7 +11,7 @@ module IncidentManagement
@issuable = issuable
@param_errors = []
- super(project: issuable.project, current_user: current_user, params: Hash(params))
+ super(project: issuable.project, current_user: current_user, params: params)
end
def execute
diff --git a/app/services/incident_management/timeline_events/create_service.rb b/app/services/incident_management/timeline_events/create_service.rb
index 40ce9097c88..5422b4ad6d2 100644
--- a/app/services/incident_management/timeline_events/create_service.rb
+++ b/app/services/incident_management/timeline_events/create_service.rb
@@ -109,7 +109,6 @@ module IncidentManagement
def add_system_note(timeline_event)
return if auto_created
- return unless Feature.enabled?(:incident_timeline, project)
SystemNoteService.add_timeline_event(timeline_event)
end
diff --git a/app/services/incident_management/timeline_events/destroy_service.rb b/app/services/incident_management/timeline_events/destroy_service.rb
index 90e95ae8869..e1c6bbbdb85 100644
--- a/app/services/incident_management/timeline_events/destroy_service.rb
+++ b/app/services/incident_management/timeline_events/destroy_service.rb
@@ -30,8 +30,6 @@ module IncidentManagement
attr_reader :project, :timeline_event, :user, :incident
def add_system_note(incident, user)
- return unless Feature.enabled?(:incident_timeline, project)
-
SystemNoteService.delete_timeline_event(incident, user)
end
end
diff --git a/app/services/incident_management/timeline_events/update_service.rb b/app/services/incident_management/timeline_events/update_service.rb
index 5c5de4717bc..012e2f0e260 100644
--- a/app/services/incident_management/timeline_events/update_service.rb
+++ b/app/services/incident_management/timeline_events/update_service.rb
@@ -38,8 +38,6 @@ module IncidentManagement
end
def add_system_note(timeline_event)
- return unless Feature.enabled?(:incident_timeline, incident.project)
-
changes = was_changed(timeline_event)
return if changes == :none
diff --git a/app/services/issuable/import_csv/base_service.rb b/app/services/issuable/import_csv/base_service.rb
index 822e3cd787c..e84d1032e41 100644
--- a/app/services/issuable/import_csv/base_service.rb
+++ b/app/services/issuable/import_csv/base_service.rb
@@ -23,7 +23,7 @@ module Issuable
with_csv_lines.each do |row, line_no|
attributes = issuable_attributes_for(row)
- if create_issuable(attributes).persisted?
+ if create_issuable(attributes)&.persisted?
@results[:success] += 1
else
@results[:error_lines].push(line_no)
diff --git a/app/services/issuable/process_assignees.rb b/app/services/issuable/process_assignees.rb
index 1ef6d3d9c42..72f727c134d 100644
--- a/app/services/issuable/process_assignees.rb
+++ b/app/services/issuable/process_assignees.rb
@@ -6,11 +6,11 @@
module Issuable
class ProcessAssignees
def initialize(assignee_ids:, add_assignee_ids:, remove_assignee_ids:, existing_assignee_ids: nil, extra_assignee_ids: nil)
- @assignee_ids = assignee_ids
- @add_assignee_ids = add_assignee_ids
- @remove_assignee_ids = remove_assignee_ids
- @existing_assignee_ids = existing_assignee_ids || []
- @extra_assignee_ids = extra_assignee_ids || []
+ @assignee_ids = assignee_ids&.map(&:to_i)
+ @add_assignee_ids = add_assignee_ids&.map(&:to_i)
+ @remove_assignee_ids = remove_assignee_ids&.map(&:to_i)
+ @existing_assignee_ids = existing_assignee_ids&.map(&:to_i) || []
+ @extra_assignee_ids = extra_assignee_ids&.map(&:to_i) || []
end
def execute
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 70ad97f8436..e24ae8f59f0 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -67,22 +67,14 @@ class IssuableBaseService < ::BaseProjectService
end
def filter_assignees(issuable)
- filter_assignees_with_key(issuable, :assignee_ids, :assignees)
- filter_assignees_with_key(issuable, :add_assignee_ids, :add_assignees)
- filter_assignees_with_key(issuable, :remove_assignee_ids, :remove_assignees)
+ filter_assignees_using_checks(issuable, :assignee_ids)
+ filter_assignees_using_checks(issuable, :add_assignee_ids)
+ filter_assignees_using_checks(issuable, :remove_assignee_ids)
end
- def filter_assignees_with_key(issuable, id_key, key)
- if params[key] && params[id_key].blank?
- params[id_key] = params[key].map(&:id)
- end
-
+ def filter_assignees_using_checks(issuable, id_key)
return if params[id_key].blank?
- filter_assignees_using_checks(issuable, id_key)
- end
-
- def filter_assignees_using_checks(issuable, id_key)
unless issuable.allows_multiple_assignees?
params[id_key] = params[id_key].first(1)
end
@@ -154,10 +146,13 @@ class IssuableBaseService < ::BaseProjectService
end
def filter_escalation_status(issuable)
+ status_params = params.delete(:escalation_status) || {}
+ status_params.permit! if status_params.respond_to?(:permit!)
+
result = ::IncidentManagement::IssuableEscalationStatuses::PrepareUpdateService.new(
issuable,
current_user,
- params.delete(:escalation_status)
+ status_params
).execute
return unless result.success? && result[:escalation_status].present?
@@ -266,11 +261,23 @@ class IssuableBaseService < ::BaseProjectService
# To be overridden by subclasses
end
- def after_update(issuable)
+ def prepare_update_params(issuable)
# To be overridden by subclasses
end
+ def after_update(issuable, old_associations)
+ handle_description_updated(issuable)
+ handle_label_changes(issuable, old_associations[:labels])
+ end
+
+ def handle_description_updated(issuable)
+ return unless issuable.previous_changes.include?('description')
+
+ GraphqlTriggers.issuable_description_updated(issuable)
+ end
+
def update(issuable)
+ prepare_update_params(issuable)
handle_quick_actions(issuable)
filter_params(issuable)
@@ -316,7 +323,7 @@ class IssuableBaseService < ::BaseProjectService
affected_assignees = (old_associations[:assignees] + new_assignees) - (old_associations[:assignees] & new_assignees)
invalidate_cache_counts(issuable, users: affected_assignees.compact)
- after_update(issuable)
+ after_update(issuable, old_associations)
issuable.create_new_cross_references!(current_user)
execute_hooks(
issuable,
@@ -356,7 +363,8 @@ class IssuableBaseService < ::BaseProjectService
handle_task_changes(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees.to_a)
- after_update(issuable)
+ # not passing old_associations here to keep `update_task` as fast as possible
+ after_update(issuable, {})
execute_hooks(issuable, 'update', old_associations: nil)
if issuable.is_a?(MergeRequest)
@@ -531,6 +539,8 @@ class IssuableBaseService < ::BaseProjectService
end
def has_label_changes?(issuable, old_labels)
+ return false if old_labels.nil?
+
Set.new(issuable.labels) != Set.new(old_labels)
end
@@ -542,12 +552,15 @@ class IssuableBaseService < ::BaseProjectService
# override if needed
def handle_label_changes(issuable, old_labels)
- return unless has_label_changes?(issuable, old_labels)
+ return false unless has_label_changes?(issuable, old_labels)
# reset to preserve the label sort order (title ASC)
issuable.labels.reset
GraphqlTriggers.issuable_labels_updated(issuable)
+
+ # return true here to avoid checking for label changes in sub classes
+ true
end
# override if needed
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index d75e74f3b19..28ea6b0ebf8 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -68,6 +68,19 @@ module Issues
rebalance_if_needed(issue)
end
+ def handle_escalation_status_change(issue)
+ return unless issue.supports_escalation?
+
+ if issue.escalation_status
+ ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(
+ issue,
+ current_user
+ ).execute
+ else
+ ::IncidentManagement::IssuableEscalationStatuses::CreateService.new(issue).execute
+ end
+ end
+
def issuable_for_positioning(id, positioning_scope)
return unless id
diff --git a/app/services/issues/clone_service.rb b/app/services/issues/clone_service.rb
index 07dd9a98f89..8b05a1c2acd 100644
--- a/app/services/issues/clone_service.rb
+++ b/app/services/issues/clone_service.rb
@@ -75,7 +75,16 @@ module Issues
# Skip creation of system notes for existing attributes of the issue when cloning with notes.
# The system notes of the old issue are copied over so we don't want to end up with duplicate notes.
# When cloning without notes, we want to generate system notes for the attributes that were copied.
- CreateService.new(project: target_project, current_user: current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: with_notes)
+ create_result = CreateService.new(
+ project: target_project,
+ current_user: current_user,
+ params: new_params,
+ spam_params: spam_params
+ ).execute(skip_system_notes: with_notes)
+
+ raise CloneError, create_result.errors.join(', ') if create_result.error? && create_result[:issue].blank?
+
+ create_result[:issue]
end
def queue_copy_designs
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 92cf4811439..89b35bbab24 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -4,6 +4,7 @@ module Issues
class CreateService < Issues::BaseService
include ResolveDiscussions
prepend RateLimitedService
+ include ::Services::ReturnServiceResponses
rate_limit key: :issues_create,
opts: { scope: [:project, :current_user, :external_author] }
@@ -20,6 +21,8 @@ module Issues
end
def execute(skip_system_notes: false)
+ return error(_('Operation not allowed'), 403) unless @current_user.can?(authorization_action, @project)
+
@issue = @build_service.execute
handle_move_between_ids(@issue)
@@ -27,7 +30,13 @@ module Issues
@add_related_issue ||= params.delete(:add_related_issue)
filter_resolve_discussion_params
- create(@issue, skip_system_notes: skip_system_notes)
+ issue = create(@issue, skip_system_notes: skip_system_notes)
+
+ if issue.persisted?
+ success(issue: issue)
+ else
+ error(issue.errors.full_messages, 422, pass_back: { issue: issue })
+ end
end
def external_author
@@ -47,7 +56,7 @@ module Issues
issue.run_after_commit do
NewIssueWorker.perform_async(issue.id, user.id, issue.class.to_s)
Issues::PlacementWorker.perform_async(nil, issue.project_id)
- Namespaces::OnboardingIssueCreatedWorker.perform_async(issue.project.namespace_id)
+ Onboarding::IssueCreatedWorker.perform_async(issue.project.namespace_id)
end
end
@@ -56,7 +65,7 @@ module Issues
user_agent_detail_service.create
handle_add_related_issue(issue)
resolve_discussions_with_issue(issue)
- create_escalation_status(issue)
+ handle_escalation_status_change(issue)
create_timeline_event(issue)
try_to_associate_contacts(issue)
@@ -87,12 +96,12 @@ module Issues
private
- attr_reader :spam_params, :extra_params
-
- def create_escalation_status(issue)
- ::IncidentManagement::IssuableEscalationStatuses::CreateService.new(issue).execute if issue.supports_escalation?
+ def authorization_action
+ :create_issue
end
+ attr_reader :spam_params, :extra_params
+
def create_timeline_event(issue)
return unless issue.incident?
diff --git a/app/services/issues/import_csv_service.rb b/app/services/issues/import_csv_service.rb
index bce3ecc8bef..83e550583f6 100644
--- a/app/services/issues/import_csv_service.rb
+++ b/app/services/issues/import_csv_service.rb
@@ -14,6 +14,10 @@ module Issues
private
+ def create_issuable(attributes)
+ super[:issue]
+ end
+
def create_issuable_class
Issues::CreateService
end
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index edab62b1fdf..6366ff4076b 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -83,7 +83,16 @@ module Issues
# Skip creation of system notes for existing attributes of the issue. The system notes of the old
# issue are copied over so we don't want to end up with duplicate notes.
- CreateService.new(project: @target_project, current_user: @current_user, params: new_params, spam_params: spam_params).execute(skip_system_notes: true)
+ create_result = CreateService.new(
+ project: @target_project,
+ current_user: @current_user,
+ params: new_params,
+ spam_params: spam_params
+ ).execute(skip_system_notes: true)
+
+ raise MoveError, create_result.errors.join(', ') if create_result.error? && create_result[:issue].blank?
+
+ create_result[:issue]
end
def queue_copy_designs
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 46c28d82ddc..e5feb4422f6 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -63,7 +63,6 @@ module Issues
handle_assignee_changes(issue, old_assignees)
handle_confidential_change(issue)
- handle_label_changes(issue, old_labels)
handle_added_labels(issue, old_labels)
handle_milestone_change(issue)
handle_added_mentions(issue, old_mentioned_users)
@@ -201,15 +200,6 @@ module Issues
::IncidentManagement::AddSeveritySystemNoteWorker.perform_async(issue.id, current_user.id)
end
- def handle_escalation_status_change(issue)
- return unless issue.supports_escalation? && issue.escalation_status
-
- ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(
- issue,
- current_user
- ).execute
- end
-
def create_confidentiality_note(issue)
SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user)
end
diff --git a/app/services/jira_connect/create_asymmetric_jwt_service.rb b/app/services/jira_connect/create_asymmetric_jwt_service.rb
new file mode 100644
index 00000000000..71aba6feddd
--- /dev/null
+++ b/app/services/jira_connect/create_asymmetric_jwt_service.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module JiraConnect
+ class CreateAsymmetricJwtService
+ ARGUMENT_ERROR_MESSAGE = 'jira_connect_installation is not a proxy installation'
+
+ def initialize(jira_connect_installation)
+ raise ArgumentError, ARGUMENT_ERROR_MESSAGE unless jira_connect_installation.proxy?
+
+ @jira_connect_installation = jira_connect_installation
+ end
+
+ def execute
+ JWT.encode(jwt_claims, private_key, 'RS256', jwt_headers)
+ end
+
+ private
+
+ def jwt_claims
+ { aud: aud_claim, iss: iss_claim, qsh: qsh_claim }
+ end
+
+ def aud_claim
+ @jira_connect_installation.audience_url
+ end
+
+ def iss_claim
+ @jira_connect_installation.client_key
+ end
+
+ def qsh_claim
+ Atlassian::Jwt.create_query_string_hash(
+ @jira_connect_installation.audience_installed_event_url,
+ 'POST',
+ @jira_connect_installation.audience_url
+ )
+ end
+
+ def private_key
+ @private_key ||= OpenSSL::PKey::RSA.generate(3072)
+ end
+
+ def public_key_storage
+ @public_key_storage ||= JiraConnect::PublicKey.create!(key: private_key.public_key)
+ end
+
+ def jwt_headers
+ { kid: public_key_storage.uuid }
+ end
+ end
+end
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
index e3b110f8f26..2786a2e357e 100644
--- a/app/services/labels/promote_service.rb
+++ b/app/services/labels/promote_service.rb
@@ -9,7 +9,7 @@ module Labels
return unless project.group &&
label.is_a?(ProjectLabel)
- Label.transaction do
+ ProjectLabel.transaction do
# use the existing group label if it exists
group_label = find_or_create_group_label(label)
@@ -50,7 +50,7 @@ module Labels
.new(current_user, title: group_label.title, group_id: project.group.id)
.execute(skip_authorization: true)
.where.not(id: group_label)
- .select(:id) # Can't use pluck() to avoid object-creation because of the batching
+ .select(:id, :project_id, :group_id, :type) # Can't use pluck() to avoid object-creation because of the batching
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 38bebc1d09d..aba075c3644 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -179,7 +179,7 @@ module Members
def enqueue_onboarding_progress_action
return unless member_created_namespace_id
- Namespaces::OnboardingUserAddedWorker.perform_async(member_created_namespace_id)
+ Onboarding::UserAddedWorker.perform_async(member_created_namespace_id)
end
def result
@@ -195,6 +195,8 @@ module Members
end
def publish_event!
+ return unless member_created_namespace_id
+
Gitlab::EventStore.publish(
Members::MembersAddedEvent.new(data: {
source_id: source.id,
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 0a8344c58db..ce79907e8a8 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -2,6 +2,8 @@
module Members
class DestroyService < Members::BaseService
+ include Gitlab::ExclusiveLeaseHelpers
+
def execute(member, skip_authorization: false, skip_subresources: false, unassign_issuables: false, destroy_bot: false)
unless skip_authorization
raise Gitlab::Access::AccessDeniedError unless authorized?(member, destroy_bot)
@@ -11,13 +13,26 @@ module Members
end
@skip_auth = skip_authorization
+ last_owner = true
+
+ in_lock("delete_members:#{member.source.class}:#{member.source.id}") do
+ break if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
+
+ last_owner = false
+ member.destroy
+ member.user&.invalidate_cache_counts
+ end
- return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
+ unless last_owner
+ delete_member_associations(member, skip_subresources, unassign_issuables)
+ end
- member.destroy
+ member
+ end
- member.user&.invalidate_cache_counts
+ private
+ def delete_member_associations(member, skip_subresources, unassign_issuables)
if member.request? && member.user != current_user
notification_service.decline_access_request(member)
end
@@ -28,12 +43,8 @@ module Members
enqueue_unassign_issuables(member) if unassign_issuables
after_execute(member: member)
-
- member
end
- private
-
def authorized?(member, destroy_bot)
return can_destroy_bot_member?(member) if destroy_bot
diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb
index 64ae33c9b15..5761e34caff 100644
--- a/app/services/merge_requests/approval_service.rb
+++ b/app/services/merge_requests/approval_service.rb
@@ -3,7 +3,7 @@
module MergeRequests
class ApprovalService < MergeRequests::BaseService
def execute(merge_request)
- return unless can_be_approved?(merge_request)
+ return unless eligible_for_approval?(merge_request)
approval = merge_request.approvals.new(user: current_user)
@@ -28,8 +28,8 @@ module MergeRequests
private
- def can_be_approved?(merge_request)
- merge_request.can_be_approved_by?(current_user)
+ def eligible_for_approval?(merge_request)
+ merge_request.eligible_for_approval_by?(current_user)
end
def save_approval(approval)
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 6cefd9169f5..cfd7c645b7e 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -58,6 +58,7 @@ module MergeRequests
new_reviewers = merge_request.reviewers - old_reviewers
merge_request_activity_counter.track_users_review_requested(users: new_reviewers)
merge_request_activity_counter.track_reviewers_changed_action(user: current_user)
+ trigger_merge_request_reviewers_updated(merge_request)
end
def cleanup_environments(merge_request)
@@ -244,6 +245,10 @@ module MergeRequests
Milestones::MergeRequestsCountService.new(milestone).delete_cache
end
+
+ def trigger_merge_request_reviewers_updated(merge_request)
+ GraphqlTriggers.merge_request_reviewers_updated(merge_request)
+ end
end
end
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index f83b14c7269..da3a9652d69 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -23,6 +23,7 @@ module MergeRequests
cleanup_environments(merge_request)
abort_auto_merge(merge_request, 'merge request was closed')
cleanup_refs(merge_request)
+ trigger_merge_request_merge_status_updated(merge_request)
end
merge_request
@@ -38,5 +39,9 @@ module MergeRequests
merge_request_metrics_service(merge_request).close(close_event)
end
end
+
+ def trigger_merge_request_merge_status_updated(merge_request)
+ GraphqlTriggers.merge_request_merge_status_updated(merge_request)
+ end
end
end
diff --git a/app/services/merge_requests/mark_reviewer_reviewed_service.rb b/app/services/merge_requests/mark_reviewer_reviewed_service.rb
index 766a4ca0a49..96747eabcf6 100644
--- a/app/services/merge_requests/mark_reviewer_reviewed_service.rb
+++ b/app/services/merge_requests/mark_reviewer_reviewed_service.rb
@@ -10,6 +10,8 @@ module MergeRequests
if reviewer
return error("Failed to update reviewer") unless reviewer.update(state: :reviewed)
+ trigger_merge_request_reviewers_updated(merge_request)
+
success
else
error("Reviewer not found")
diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb
index 3e630d40b3d..2a3c1e8bc26 100644
--- a/app/services/merge_requests/merge_base_service.rb
+++ b/app/services/merge_requests/merge_base_service.rb
@@ -9,12 +9,12 @@ module MergeRequests
attr_reader :merge_request
# Overridden in EE.
- def hooks_validation_pass?(_merge_request)
+ def hooks_validation_pass?(merge_request, validate_squash_message: false)
true
end
# Overridden in EE.
- def hooks_validation_error(_merge_request)
+ def hooks_validation_error(merge_request, validate_squash_message: false)
# No-op
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 6d31a29f5a7..6b4f9dbe509 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -26,6 +26,7 @@ module MergeRequests
@merge_request = merge_request
@options = options
+ jid = merge_jid
validate!
@@ -37,7 +38,7 @@ module MergeRequests
end
end
- log_info("Merge process finished on JID #{merge_jid} with state #{state}")
+ log_info("Merge process finished on JID #{jid} with state #{state}")
rescue MergeError => e
handle_merge_error(log_message: e.message, save_message_on_model: true)
ensure
@@ -159,17 +160,32 @@ module MergeRequests
end
def handle_merge_error(log_message:, save_message_on_model: false)
- Gitlab::AppLogger.error("MergeService ERROR: #{merge_request_info} - #{log_message}")
+ log_error("MergeService ERROR: #{merge_request_info} - #{log_message}")
@merge_request.update(merge_error: log_message) if save_message_on_model
end
def log_info(message)
+ payload = log_payload("#{merge_request_info} - #{message}")
+ logger.info(**payload)
+ end
+
+ def log_error(message)
+ payload = log_payload(message)
+ logger.error(**payload)
+ end
+
+ def logger
@logger ||= Gitlab::AppLogger
- @logger.info("#{merge_request_info} - #{message}")
+ end
+
+ def log_payload(message)
+ Gitlab::ApplicationContext.current
+ .merge(merge_request_info: merge_request_info,
+ message: message)
end
def merge_request_info
- merge_request.to_reference(full: true)
+ @merge_request_info ||= merge_request.to_reference(full: true)
end
def source_matches?
diff --git a/app/services/merge_requests/mergeability/logger.rb b/app/services/merge_requests/mergeability/logger.rb
index 8b45d231e03..88ef6d81eaa 100644
--- a/app/services/merge_requests/mergeability/logger.rb
+++ b/app/services/merge_requests/mergeability/logger.rb
@@ -11,16 +11,12 @@ module MergeRequests
end
def commit
- return unless enabled?
-
commit_logs
end
def instrument(mergeability_name:)
raise ArgumentError, 'block not given' unless block_given?
- return yield unless enabled?
-
op_start_db_counters = current_db_counter_payload
op_started_at = current_monotonic_time
@@ -38,15 +34,11 @@ module MergeRequests
attr_reader :destination, :merge_request
def observe(name, value)
- return unless enabled?
-
observations[name.to_s].push(value)
end
def commit_logs
- attributes = Gitlab::ApplicationContext.current.merge({
- mergeability_project_id: merge_request.project.id
- })
+ attributes = Gitlab::ApplicationContext.current.merge({ mergeability_project_id: merge_request.project.id })
attributes[:mergeability_merge_request_id] = merge_request.id
attributes.merge!(observations_hash)
@@ -89,12 +81,6 @@ module MergeRequests
::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_payload
end
- def enabled?
- strong_memoize(:enabled) do
- ::Feature.enabled?(:mergeability_checks_logger, merge_request.project)
- end
- end
-
def current_monotonic_time
::Gitlab::Metrics::System.monotonic_time
end
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
index ef251f121ae..aa52349b0ee 100644
--- a/app/services/merge_requests/push_options_handler_service.rb
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -104,7 +104,7 @@ module MergeRequests
merge_request = ::MergeRequests::CreateService.new(
project: project,
current_user: current_user,
- params: merge_request.attributes.merge(assignees: merge_request.assignees,
+ params: merge_request.attributes.merge(assignee_ids: merge_request.assignee_ids,
label_ids: merge_request.label_ids)
).execute
end
@@ -140,8 +140,8 @@ module MergeRequests
params[:add_labels] = params.delete(:label).keys if params.has_key?(:label)
params[:remove_labels] = params.delete(:unlabel).keys if params.has_key?(:unlabel)
- params[:add_assignee_ids] = params.delete(:assign).keys if params.has_key?(:assign)
- params[:remove_assignee_ids] = params.delete(:unassign).keys if params.has_key?(:unassign)
+ params[:add_assignee_ids] = convert_to_user_ids(params.delete(:assign).keys) if params.has_key?(:assign)
+ params[:remove_assignee_ids] = convert_to_user_ids(params.delete(:unassign).keys) if params.has_key?(:unassign)
if push_options[:milestone]
milestone = Milestone.for_projects_and_groups(@project, @project.ancestors_upto)&.find_by_name(push_options[:milestone])
@@ -169,7 +169,7 @@ module MergeRequests
params = base_params
params.merge!(
- assignees: [current_user],
+ assignee_ids: [current_user.id],
source_branch: branch,
source_project: project,
target_project: target_project
@@ -186,6 +186,12 @@ module MergeRequests
base_params.merge(merge_params(merge_request.source_branch))
end
+ def convert_to_user_ids(ids_or_usernames)
+ ids, usernames = ids_or_usernames.partition { |id_or_username| id_or_username.is_a?(Numeric) || id_or_username.match?(/\A\d+\z/) }
+ ids += User.by_username(usernames).pluck(:id) unless usernames.empty? # rubocop:disable CodeReuse/ActiveRecord
+ ids
+ end
+
def collect_errors_from_merge_request(merge_request)
merge_request.errors.full_messages.each do |error|
errors << error
diff --git a/app/services/merge_requests/request_review_service.rb b/app/services/merge_requests/request_review_service.rb
index b061ed45fee..ebbae98352b 100644
--- a/app/services/merge_requests/request_review_service.rb
+++ b/app/services/merge_requests/request_review_service.rb
@@ -11,6 +11,7 @@ module MergeRequests
return error("Failed to update reviewer") unless reviewer.update(state: :unreviewed)
notify_reviewer(merge_request, user)
+ trigger_merge_request_reviewers_updated(merge_request)
success
else
diff --git a/app/services/merge_requests/update_assignees_service.rb b/app/services/merge_requests/update_assignees_service.rb
index a13db52e34b..79a3e9f3c22 100644
--- a/app/services/merge_requests/update_assignees_service.rb
+++ b/app/services/merge_requests/update_assignees_service.rb
@@ -18,7 +18,17 @@ module MergeRequests
return merge_request if old_ids.to_set == new_ids.to_set # no-change
attrs = update_attrs.merge(assignee_ids: new_ids)
- merge_request.update!(**attrs)
+
+ # We now have assignees validation on merge request
+ # If we use an update with bang, it will explode,
+ # instead we need to check if its valid then return if its not valid.
+ if Feature.enabled?(:limit_assignees_per_issuable)
+ merge_request.update(**attrs)
+
+ return merge_request unless merge_request.valid?
+ else
+ merge_request.update!(**attrs)
+ end
# Defer the more expensive operations (handle_assignee_changes) to the background
MergeRequests::HandleAssigneesChangeService
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 6d518edc88f..745647b727c 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -38,7 +38,6 @@ module MergeRequests
handle_target_branch_change(merge_request)
handle_milestone_change(merge_request)
handle_draft_status_change(merge_request, changed_fields)
- handle_label_changes(merge_request, old_labels)
track_title_and_desc_edits(changed_fields)
track_discussion_lock_toggle(merge_request, changed_fields)
@@ -71,7 +70,8 @@ module MergeRequests
MergeRequests::CloseService
end
- def after_update(issuable)
+ def after_update(issuable, old_associations)
+ super
issuable.cache_merge_request_closes_issues!(current_user)
end
@@ -179,9 +179,12 @@ module MergeRequests
old_title_draft = MergeRequest.draft?(old_title)
new_title_draft = MergeRequest.draft?(new_title)
- # notify the draft status changed. Added/removed message is handled in the
- # email template itself, see `change_in_merge_request_draft_status_email` template.
- notify_draft_status_changed(merge_request) if old_title_draft || new_title_draft
+ if old_title_draft || new_title_draft
+ # notify the draft status changed. Added/removed message is handled in the
+ # email template itself, see `change_in_merge_request_draft_status_email` template.
+ notify_draft_status_changed(merge_request)
+ trigger_merge_request_status_updated(merge_request)
+ end
if !old_title_draft && new_title_draft
# Marked as Draft
@@ -320,6 +323,10 @@ module MergeRequests
def filter_sentinel_values(param)
param.reject { _1 == 0 }
end
+
+ def trigger_merge_request_status_updated(merge_request)
+ GraphqlTriggers.merge_request_merge_status_updated(merge_request)
+ end
end
end
diff --git a/app/services/ml/experiment_tracking/candidate_repository.rb b/app/services/ml/experiment_tracking/candidate_repository.rb
new file mode 100644
index 00000000000..b6f87995185
--- /dev/null
+++ b/app/services/ml/experiment_tracking/candidate_repository.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module Ml
+ module ExperimentTracking
+ class CandidateRepository
+ attr_accessor :project, :user, :experiment, :candidate
+
+ def initialize(project, user)
+ @project = project
+ @user = user
+ end
+
+ def by_iid(iid)
+ ::Ml::Candidate.with_project_id_and_iid(project.id, iid)
+ end
+
+ def create!(experiment, start_time)
+ experiment.candidates.create!(
+ user: user,
+ start_time: start_time || 0
+ )
+ end
+
+ def update(candidate, status, end_time)
+ candidate.status = status.downcase if status
+ candidate.end_time = end_time if end_time
+
+ candidate.save
+ end
+
+ def add_metric!(candidate, name, value, tracked_at, step)
+ candidate.metrics.create!(
+ name: name,
+ value: value,
+ tracked_at: tracked_at,
+ step: step
+ )
+ end
+
+ def add_param!(candidate, name, value)
+ candidate.params.create!(name: name, value: value)
+ end
+
+ def add_metrics(candidate, metric_definitions)
+ return unless candidate.present?
+
+ metrics = metric_definitions.map do |metric|
+ {
+ candidate_id: candidate.id,
+ name: metric[:key],
+ value: metric[:value],
+ tracked_at: metric[:timestamp],
+ step: metric[:step],
+ **timestamps
+ }
+ end
+
+ ::Ml::CandidateMetric.insert_all(metrics, returning: false) unless metrics.empty?
+ end
+
+ def add_params(candidate, param_definitions)
+ return unless candidate.present?
+
+ parameters = param_definitions.map do |p|
+ {
+ candidate_id: candidate.id,
+ name: p[:key],
+ value: p[:value],
+ **timestamps
+ }
+ end
+
+ ::Ml::CandidateParam.insert_all(parameters, returning: false) unless parameters.empty?
+ end
+
+ private
+
+ def timestamps
+ current_time = Time.zone.now
+
+ { created_at: current_time, updated_at: current_time }
+ end
+ end
+ end
+end
diff --git a/app/services/ml/experiment_tracking/experiment_repository.rb b/app/services/ml/experiment_tracking/experiment_repository.rb
new file mode 100644
index 00000000000..891674adc2a
--- /dev/null
+++ b/app/services/ml/experiment_tracking/experiment_repository.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Ml
+ module ExperimentTracking
+ class ExperimentRepository
+ attr_accessor :project, :user
+
+ def initialize(project, user = nil)
+ @project = project
+ @user = user
+ end
+
+ def by_iid_or_name(iid: nil, name: nil)
+ return ::Ml::Experiment.by_project_id_and_iid(project.id, iid) if iid
+
+ ::Ml::Experiment.by_project_id_and_name(project.id, name) if name
+ end
+
+ def all
+ ::Ml::Experiment.by_project_id(project.id)
+ end
+
+ def create!(name)
+ ::Ml::Experiment.create!(name: name,
+ user: user,
+ project: project)
+ end
+ end
+ end
+end
diff --git a/app/services/namespaces/package_settings/update_service.rb b/app/services/namespaces/package_settings/update_service.rb
index c0af0900450..0c6fcee9113 100644
--- a/app/services/namespaces/package_settings/update_service.rb
+++ b/app/services/namespaces/package_settings/update_service.rb
@@ -8,7 +8,13 @@ module Namespaces
ALLOWED_ATTRIBUTES = %i[maven_duplicates_allowed
maven_duplicate_exception_regex
generic_duplicates_allowed
- generic_duplicate_exception_regex].freeze
+ generic_duplicate_exception_regex
+ maven_package_requests_forwarding
+ npm_package_requests_forwarding
+ pypi_package_requests_forwarding
+ lock_maven_package_requests_forwarding
+ lock_npm_package_requests_forwarding
+ lock_pypi_package_requests_forwarding].freeze
def execute
return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed?
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index b7e6a50fa5c..1aaf7fb769a 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -88,12 +88,14 @@ module Notes
return if quick_actions_service.commands_executed_count.to_i == 0
if update_params.present?
- if check_for_reviewer_validity(message, update_params)
+ invalid_message = validate_commands(note, update_params)
+
+ if invalid_message
+ note.errors.add(:validation, invalid_message)
+ message = invalid_message
+ else
quick_actions_service.apply_updates(update_params, note)
note.commands_changes = update_params
- else
- message = "Reviewers #{MergeRequest.max_number_of_assignees_or_reviewers_message}"
- note.errors.add(:validation, message)
end
end
@@ -114,16 +116,36 @@ module Notes
}
end
- def check_for_reviewer_validity(message, update_params)
- return true unless Feature.enabled?(:limit_reviewer_and_assignee_size)
+ def validate_commands(note, update_params)
+ if invalid_reviewers?(update_params)
+ "Reviewers #{note.noteable.class.max_number_of_assignees_or_reviewers_message}"
+ elsif invalid_assignees?(update_params)
+ "Assignees #{note.noteable.class.max_number_of_assignees_or_reviewers_message}"
+ end
+ end
+
+ def invalid_reviewers?(update_params)
+ return false unless Feature.enabled?(:limit_reviewer_and_assignee_size)
if update_params.key?(:reviewer_ids)
possible_reviewers = update_params[:reviewer_ids]&.uniq&.size
- return false if possible_reviewers > MergeRequest::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
+ possible_reviewers > ::Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
+ else
+ false
end
+ end
+
+ def invalid_assignees?(update_params)
+ return false unless Feature.enabled?(:limit_assignees_per_issuable)
- true
+ if update_params.key?(:assignee_ids)
+ possible_assignees = update_params[:assignee_ids]&.uniq&.size
+
+ possible_assignees > ::Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
+ else
+ false
+ end
end
def track_event(note, user)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 5a92adfd25a..1224cf80b76 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -87,6 +87,13 @@ class NotificationService
mailer.access_token_expired_email(user).deliver_later
end
+ # Notify the user when one of their personal access tokens is revoked
+ def access_token_revoked(user, token_name)
+ return unless user.can?(:receive_notifications)
+
+ mailer.access_token_revoked_email(user, token_name).deliver_later
+ end
+
# Notify the user when at least one of their ssh key has expired today
def ssh_key_expired(user, fingerprints)
return unless user.can?(:receive_notifications)
@@ -109,6 +116,14 @@ class NotificationService
mailer.unknown_sign_in_email(user, ip, time).deliver_later
end
+ # Notify a user when a wrong 2FA OTP has been entered to
+ # try to sign in to their account
+ def two_factor_otp_attempt_failed(user, ip)
+ return unless user.can?(:receive_notifications)
+
+ mailer.two_factor_otp_attempt_failed_email(user, ip).deliver_later
+ end
+
# Notify a user when a new email address is added to the their account
def new_email_address_added(user, email)
return unless user.can?(:receive_notifications)
diff --git a/app/services/onboarding/progress_service.rb b/app/services/onboarding/progress_service.rb
index 66f7f2bc33d..c67669b49ab 100644
--- a/app/services/onboarding/progress_service.rb
+++ b/app/services/onboarding/progress_service.rb
@@ -12,7 +12,7 @@ module Onboarding
def execute(action:)
return unless Onboarding::Progress.not_completed?(namespace_id, action)
- Namespaces::OnboardingProgressWorker.perform_async(namespace_id, action)
+ Onboarding::ProgressWorker.perform_async(namespace_id, action)
end
end
diff --git a/app/services/packages/debian/create_package_file_service.rb b/app/services/packages/debian/create_package_file_service.rb
index 53275fdc9bb..19e68183ea2 100644
--- a/app/services/packages/debian/create_package_file_service.rb
+++ b/app/services/packages/debian/create_package_file_service.rb
@@ -5,18 +5,20 @@ module Packages
class CreatePackageFileService
include ::Packages::FIPS
- def initialize(package, params)
+ def initialize(package:, current_user:, params: {})
@package = package
+ @current_user = current_user
@params = params
end
def execute
raise DisabledError, 'Debian registry is not FIPS compliant' if Gitlab::FIPS.enabled?
raise ArgumentError, "Invalid package" unless package.present?
+ raise ArgumentError, "Invalid user" unless current_user.present?
# Debian package file are first uploaded to incoming with empty metadata,
# and are moved later by Packages::Debian::ProcessChangesService
- package.package_files.create!(
+ package_file = package.package_files.create!(
file: params[:file],
size: params[:file]&.size,
file_name: params[:file_name],
@@ -29,11 +31,17 @@ module Packages
fields: nil
}
)
+
+ if params[:file_name].end_with? '.changes'
+ ::Packages::Debian::ProcessChangesWorker.perform_async(package_file.id, current_user.id)
+ end
+
+ package_file
end
private
- attr_reader :package, :params
+ attr_reader :package, :current_user, :params
end
end
end
diff --git a/app/services/packages/mark_packages_for_destruction_service.rb b/app/services/packages/mark_packages_for_destruction_service.rb
new file mode 100644
index 00000000000..023392cf2d9
--- /dev/null
+++ b/app/services/packages/mark_packages_for_destruction_service.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module Packages
+ class MarkPackagesForDestructionService
+ include BaseServiceUtility
+
+ BATCH_SIZE = 20
+
+ UNAUTHORIZED_RESPONSE = ServiceResponse.error(
+ message: "You don't have the permission to perform this action",
+ reason: :unauthorized
+ ).freeze
+
+ ERROR_RESPONSE = ServiceResponse.error(
+ message: 'Failed to mark the packages as pending destruction'
+ ).freeze
+
+ SUCCESS_RESPONSE = ServiceResponse.success(
+ message: 'Packages were successfully marked as pending destruction'
+ ).freeze
+
+ # Initialize this service with the given packages and user.
+ #
+ # * `packages`: must be an ActiveRecord relationship.
+ # * `current_user`: an User object. Could be nil.
+ def initialize(packages:, current_user: nil)
+ @packages = packages
+ @current_user = current_user
+ end
+
+ def execute(batch_size: BATCH_SIZE)
+ no_access = false
+ min_batch_size = [batch_size, BATCH_SIZE].min
+
+ @packages.each_batch(of: min_batch_size) do |batched_packages|
+ loaded_packages = batched_packages.including_project_route.to_a
+
+ break no_access = true unless can_destroy_packages?(loaded_packages)
+
+ ::Packages::Package.id_in(loaded_packages.map(&:id))
+ .update_all(status: :pending_destruction)
+
+ sync_maven_metadata(loaded_packages)
+ mark_package_files_for_destruction(loaded_packages)
+ end
+
+ return UNAUTHORIZED_RESPONSE if no_access
+
+ SUCCESS_RESPONSE
+ rescue StandardError
+ ERROR_RESPONSE
+ end
+
+ private
+
+ def mark_package_files_for_destruction(packages)
+ ::Packages::MarkPackageFilesForDestructionWorker.bulk_perform_async_with_contexts(
+ packages,
+ arguments_proc: -> (package) { package.id },
+ context_proc: -> (package) { { project: package.project, user: @current_user } }
+ )
+ end
+
+ def sync_maven_metadata(packages)
+ maven_packages_with_version = packages.select { |pkg| pkg.maven? && pkg.version? }
+ ::Packages::Maven::Metadata::SyncWorker.bulk_perform_async_with_contexts(
+ maven_packages_with_version,
+ arguments_proc: -> (package) { [@current_user.id, package.project_id, package.name] },
+ context_proc: -> (package) { { project: package.project, user: @current_user } }
+ )
+ end
+
+ def can_destroy_packages?(packages)
+ packages.all? do |package|
+ can?(@current_user, :destroy_package, package)
+ end
+ end
+ end
+end
diff --git a/app/services/packages/rpm/parse_package_service.rb b/app/services/packages/rpm/parse_package_service.rb
new file mode 100644
index 00000000000..689a161a81a
--- /dev/null
+++ b/app/services/packages/rpm/parse_package_service.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module Packages
+ module Rpm
+ class ParsePackageService
+ include ::Gitlab::Utils::StrongMemoize
+
+ BUILD_ATTRIBUTES_METHOD_NAMES = %i[changelogs requirements provides].freeze
+ STATIC_ATTRIBUTES = %i[name version release summary description arch
+ license sourcerpm group buildhost packager vendor].freeze
+
+ CHANGELOGS_RPM_KEYS = %i[changelogtext changelogtime].freeze
+ REQUIREMENTS_RPM_KEYS = %i[requirename requireversion requireflags].freeze
+ PROVIDES_RPM_KEYS = %i[providename provideflags provideversion].freeze
+
+ def initialize(package_file)
+ @rpm = RPM::File.new(package_file)
+ end
+
+ def execute
+ raise ArgumentError, 'Unable to parse package' unless valid_package?
+
+ {
+ files: rpm.files || [],
+ epoch: package_tags[:epoch] || '0',
+ changelogs: build_changelogs,
+ requirements: build_requirements,
+ provides: build_provides
+ }.merge(extract_static_attributes)
+ end
+
+ private
+
+ attr_reader :rpm
+
+ def valid_package?
+ rpm.files && package_tags && true
+ rescue RuntimeError
+ # if arr-pm throws an error due to an incorrect file format,
+ # we just want this validation to fail rather than throw an exception
+ false
+ end
+
+ def package_tags
+ strong_memoize(:package_tags) do
+ rpm.tags
+ end
+ end
+
+ def extract_static_attributes
+ STATIC_ATTRIBUTES.each_with_object({}) do |attribute, hash|
+ hash[attribute] = package_tags[attribute]
+ end
+ end
+
+ # Define methods for building RPM attribute data from parsed package
+ # Transform
+ # changelogtime: [123, 234],
+ # changelogname: ["First", "Second"]
+ # changelogtext: ["Work1", "Work2"]
+ # Into
+ # changelog: [
+ # {changelogname: "First", changelogtext: "Work1", changelogtime: 123},
+ # {changelogname: "Second", changelogtext: "Work2", changelogtime: 234}
+ # ]
+ BUILD_ATTRIBUTES_METHOD_NAMES.each do |resource|
+ define_method("build_#{resource}") do
+ resource_keys = self.class.const_get("#{resource.upcase}_RPM_KEYS", false).dup
+ return [] if resource_keys.any? { package_tags[_1].blank? }
+
+ first_attributes = package_tags[resource_keys.first]
+ zipped_data = first_attributes.zip(*resource_keys[1..].map { package_tags[_1] })
+ build_hashes(resource_keys, zipped_data)
+ end
+ end
+
+ def build_hashes(resource_keys, zipped_data)
+ zipped_data.map do |data|
+ resource_keys.zip(data).to_h
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/packages/rpm/repository_metadata/base_builder.rb b/app/services/packages/rpm/repository_metadata/base_builder.rb
index 9d76336d764..2c0a11457ec 100644
--- a/app/services/packages/rpm/repository_metadata/base_builder.rb
+++ b/app/services/packages/rpm/repository_metadata/base_builder.rb
@@ -3,17 +3,43 @@ module Packages
module Rpm
module RepositoryMetadata
class BaseBuilder
+ def initialize(xml: nil, data: {})
+ @xml = Nokogiri::XML(xml) if xml.present?
+ @data = data
+ end
+
def execute
- build_empty_structure
+ return build_empty_structure if xml.blank?
+
+ update_xml_document
+ update_package_count
+ xml.to_xml
end
private
+ attr_reader :xml, :data
+
def build_empty_structure
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
- xml.public_send(self.class::ROOT_TAG, self.class::ROOT_ATTRIBUTES) # rubocop:disable GitlabSecurity/PublicSend
+ xml.method_missing(self.class::ROOT_TAG, self.class::ROOT_ATTRIBUTES)
end.to_xml
end
+
+ def update_xml_document
+ # Add to the root xml element a new package metadata node
+ xml.at(self.class::ROOT_TAG).add_child(build_new_node)
+ end
+
+ def update_package_count
+ packages_count = xml.css("//#{self.class::ROOT_TAG}/package").count
+
+ xml.at(self.class::ROOT_TAG).attributes["packages"].value = packages_count.to_s
+ end
+
+ def build_new_node
+ raise NotImplementedError, "#{self.class} should implement #{__method__}"
+ end
end
end
end
diff --git a/app/services/packages/rpm/repository_metadata/build_primary_xml.rb b/app/services/packages/rpm/repository_metadata/build_primary_xml.rb
index affb41677c2..580bf844a0c 100644
--- a/app/services/packages/rpm/repository_metadata/build_primary_xml.rb
+++ b/app/services/packages/rpm/repository_metadata/build_primary_xml.rb
@@ -9,6 +9,79 @@ module Packages
'xmlns:rpm': 'http://linux.duke.edu/metadata/rpm',
packages: '0'
}.freeze
+
+ # Nodes that have only text without attributes
+ REQUIRED_BASE_ATTRIBUTES = %i[name arch summary description].freeze
+ NOT_REQUIRED_BASE_ATTRIBUTES = %i[url packager].freeze
+ FORMAT_NODE_BASE_ATTRIBUTES = %i[license vendor group buildhost sourcerpm].freeze
+
+ private
+
+ def build_new_node
+ builder = Nokogiri::XML::Builder.new do |xml|
+ xml.package(type: :rpm, 'xmlns:rpm': 'http://linux.duke.edu/metadata/rpm') do
+ build_required_base_attributes(xml)
+ build_not_required_base_attributes(xml)
+ xml.version epoch: data[:epoch], ver: data[:version], rel: data[:release]
+ xml.checksum data[:checksum], type: 'sha256', pkgid: 'YES'
+ xml.size package: data[:packagesize], installed: data[:installedsize], archive: data[:archivesize]
+ xml.time file: data[:filetime], build: data[:buildtime]
+ xml.location href: data[:location] if data[:location].present?
+ build_format_node(xml)
+ end
+ end
+
+ Nokogiri::XML(builder.to_xml).at('package')
+ end
+
+ def build_required_base_attributes(xml)
+ REQUIRED_BASE_ATTRIBUTES.each do |attribute|
+ xml.method_missing(attribute, data[attribute])
+ end
+ end
+
+ def build_not_required_base_attributes(xml)
+ NOT_REQUIRED_BASE_ATTRIBUTES.each do |attribute|
+ xml.method_missing(attribute, data[attribute]) if data[attribute].present?
+ end
+ end
+
+ def build_format_node(xml)
+ xml.format do
+ build_base_format_attributes(xml)
+ build_provides_node(xml)
+ build_requires_node(xml)
+ end
+ end
+
+ def build_base_format_attributes(xml)
+ FORMAT_NODE_BASE_ATTRIBUTES.each do |attribute|
+ xml[:rpm].method_missing(attribute, data[attribute]) if data[attribute].present?
+ end
+ end
+
+ def build_requires_node(xml)
+ xml[:rpm].requires do
+ data[:requirements].each do |requires|
+ xml[:rpm].entry(
+ name: requires[:requirename],
+ flags: requires[:requireflags],
+ ver: requires[:requireversion]
+ )
+ end
+ end
+ end
+
+ def build_provides_node(xml)
+ xml[:rpm].provides do
+ data[:provides].each do |provides|
+ xml[:rpm].entry(
+ name: provides[:providename],
+ flags: provides[:provideflags],
+ ver: provides[:provideversion])
+ end
+ end
+ end
end
end
end
diff --git a/app/services/packages/rpm/repository_metadata/build_repomd_xml.rb b/app/services/packages/rpm/repository_metadata/build_repomd_xml.rb
index c6cfd77815d..84614196254 100644
--- a/app/services/packages/rpm/repository_metadata/build_repomd_xml.rb
+++ b/app/services/packages/rpm/repository_metadata/build_repomd_xml.rb
@@ -9,6 +9,7 @@ module Packages
xmlns: 'http://linux.duke.edu/metadata/repo',
'xmlns:rpm': 'http://linux.duke.edu/metadata/rpm'
}.freeze
+ ALLOWED_DATA_VALUE_KEYS = %i[checksum open-checksum location timestamp size open-size].freeze
# Expected `data` structure
#
@@ -48,9 +49,9 @@ module Packages
end
def build_file_info(info, xml)
- info.each do |key, attributes|
+ info.slice(*ALLOWED_DATA_VALUE_KEYS).each do |key, attributes|
value = attributes.delete(:value)
- xml.public_send(key, value, attributes) # rubocop:disable GitlabSecurity/PublicSend
+ xml.method_missing(key, value, attributes)
end
end
end
diff --git a/app/services/pages_domains/create_acme_order_service.rb b/app/services/pages_domains/create_acme_order_service.rb
index e289a78091b..c600f497fa5 100644
--- a/app/services/pages_domains/create_acme_order_service.rb
+++ b/app/services/pages_domains/create_acme_order_service.rb
@@ -2,9 +2,6 @@
module PagesDomains
class CreateAcmeOrderService
- # elliptic curve algorithm to generate the private key
- ECDSA_CURVE = "prime256v1"
-
attr_reader :pages_domain
def initialize(pages_domain)
@@ -17,12 +14,7 @@ module PagesDomains
challenge = order.new_challenge
- private_key = if Feature.enabled?(:pages_lets_encrypt_ecdsa, pages_domain.project)
- OpenSSL::PKey::EC.generate(ECDSA_CURVE)
- else
- OpenSSL::PKey::RSA.new(4096)
- end
-
+ private_key = OpenSSL::PKey::RSA.new(4096)
saved_order = pages_domain.acme_orders.create!(
url: order.url,
expires_at: order.expires,
diff --git a/app/services/pages_domains/create_service.rb b/app/services/pages_domains/create_service.rb
new file mode 100644
index 00000000000..1f771ca3a05
--- /dev/null
+++ b/app/services/pages_domains/create_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module PagesDomains
+ class CreateService < BaseService
+ def execute
+ return unless authorized?
+
+ domain = project.pages_domains.create(params)
+
+ publish_event(domain) if domain.persisted?
+
+ domain
+ end
+
+ private
+
+ def authorized?
+ current_user.can?(:update_pages, project)
+ end
+
+ def publish_event(domain)
+ event = PagesDomainCreatedEvent.new(
+ data: {
+ project_id: project.id,
+ namespace_id: project.namespace_id,
+ root_namespace_id: project.root_namespace.id,
+ domain: domain.domain
+ }
+ )
+
+ Gitlab::EventStore.publish(event)
+ end
+ end
+end
diff --git a/app/services/pages_domains/delete_service.rb b/app/services/pages_domains/delete_service.rb
new file mode 100644
index 00000000000..af69e1845a9
--- /dev/null
+++ b/app/services/pages_domains/delete_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module PagesDomains
+ class DeleteService < BaseService
+ def execute(domain)
+ return unless authorized?
+
+ domain.destroy
+
+ publish_event(domain)
+ end
+
+ private
+
+ def authorized?
+ current_user.can?(:update_pages, project)
+ end
+
+ def publish_event(domain)
+ event = PagesDomainDeletedEvent.new(
+ data: {
+ project_id: project.id,
+ namespace_id: project.namespace_id,
+ root_namespace_id: project.root_namespace.id,
+ domain: domain.domain
+ }
+ )
+
+ Gitlab::EventStore.publish(event)
+ end
+ end
+end
diff --git a/app/services/pages_domains/update_service.rb b/app/services/pages_domains/update_service.rb
new file mode 100644
index 00000000000..b038aaa95b6
--- /dev/null
+++ b/app/services/pages_domains/update_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module PagesDomains
+ class UpdateService < BaseService
+ def execute(domain)
+ return unless authorized?
+
+ return false unless domain.update(params)
+
+ publish_event(domain)
+
+ true
+ end
+
+ private
+
+ def authorized?
+ current_user.can?(:update_pages, project)
+ end
+
+ def publish_event(domain)
+ event = PagesDomainUpdatedEvent.new(
+ data: {
+ project_id: project.id,
+ namespace_id: project.namespace_id,
+ root_namespace_id: project.root_namespace.id,
+ domain: domain.domain
+ }
+ )
+
+ Gitlab::EventStore.publish(event)
+ end
+ end
+end
diff --git a/app/services/personal_access_tokens/revoke_service.rb b/app/services/personal_access_tokens/revoke_service.rb
index 0275d03bcc9..732da75da3a 100644
--- a/app/services/personal_access_tokens/revoke_service.rb
+++ b/app/services/personal_access_tokens/revoke_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module PersonalAccessTokens
- class RevokeService
+ class RevokeService < BaseService
attr_reader :token, :current_user, :group
def initialize(current_user = nil, token: nil, group: nil )
@@ -15,6 +15,7 @@ module PersonalAccessTokens
if token.revoke!
log_event
+ notification_service.access_token_revoked(token.user, token.name)
ServiceResponse.success(message: success_message)
else
ServiceResponse.error(message: error_message)
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index e6b1b33a82a..ae5aae87a77 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -24,7 +24,7 @@ module Projects
end
def commands(noteable, type)
- return [] unless noteable
+ return [] unless noteable && current_user
QuickActions::InterpretService.new(project, current_user).available_commands(noteable)
end
@@ -33,9 +33,21 @@ module Projects
SnippetsFinder.new(current_user, project: project).execute.select([:id, :title])
end
- def contacts
- Crm::ContactsFinder.new(current_user, group: project.group).execute
- .select([:id, :email, :first_name, :last_name])
+ def contacts(target)
+ available_contacts = Crm::ContactsFinder.new(current_user, group: project.group).execute
+ .select([:id, :email, :first_name, :last_name, :state])
+
+ contact_hashes = available_contacts.as_json
+
+ return contact_hashes unless target.is_a?(Issue)
+
+ ids = target.customer_relations_contacts.ids # rubocop:disable CodeReuse/ActiveRecord
+
+ contact_hashes.each do |hash|
+ hash[:set] = ids.include?(hash['id'])
+ end
+
+ contact_hashes
end
def labels_as_hash(target)
diff --git a/app/services/projects/blame_service.rb b/app/services/projects/blame_service.rb
index 57b913b04e6..58e146e5a32 100644
--- a/app/services/projects/blame_service.rb
+++ b/app/services/projects/blame_service.rb
@@ -27,6 +27,10 @@ module Projects
.page(page)
end
+ def per_page
+ PER_PAGE
+ end
+
private
attr_reader :blob, :commit, :pagination_enabled
@@ -48,10 +52,6 @@ module Projects
page
end
- def per_page
- PER_PAGE
- end
-
def pagination_state(params)
return false if Gitlab::Utils.to_boolean(params[:no_pagination], default: false)
diff --git a/app/services/projects/container_repository/cleanup_tags_base_service.rb b/app/services/projects/container_repository/cleanup_tags_base_service.rb
index 8ea4ae4830a..5393c2c080d 100644
--- a/app/services/projects/container_repository/cleanup_tags_base_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_base_service.rb
@@ -60,23 +60,6 @@ module Projects
service.execute(container_repository)
end
- def can_destroy?
- return true if container_expiration_policy
-
- can?(current_user, :destroy_container_image, project)
- end
-
- def valid_regex?
- %w[name_regex_delete name_regex name_regex_keep].each do |param_name|
- regex = params[param_name]
- ::Gitlab::UntrustedRegexp.new(regex) unless regex.blank?
- end
- true
- rescue RegexpError => e
- ::Gitlab::ErrorTracking.log_exception(e, project_id: project.id)
- false
- end
-
def older_than
params['older_than']
end
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
index 285c3e252ef..cf2eb81e5f3 100644
--- a/app/services/projects/container_repository/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -2,101 +2,57 @@
module Projects
module ContainerRepository
- class CleanupTagsService < CleanupTagsBaseService
- def initialize(container_repository:, current_user: nil, params: {})
- super
-
- @params = params.dup
- @counts = { cached_tags_count: 0 }
- end
-
+ class CleanupTagsService < BaseContainerRepositoryService
def execute
return error('access denied') unless can_destroy?
return error('invalid regex') unless valid_regex?
- tags = container_repository.tags
- @counts[:original_size] = tags.size
-
- filter_out_latest!(tags)
- filter_by_name!(tags)
-
- tags = truncate(tags)
- populate_from_cache(tags)
-
- tags = filter_keep_n(tags)
- tags = filter_by_older_than(tags)
-
- @counts[:before_delete_size] = tags.size
-
- delete_tags(tags).merge(@counts).tap do |result|
- result[:deleted_size] = result[:deleted]&.size
-
- result[:status] = :error if @counts[:before_truncate_size] != @counts[:after_truncate_size]
- end
+ cleanup_tags_service_class.new(container_repository: container_repository, current_user: current_user, params: params)
+ .execute
end
private
- def filter_keep_n(tags)
- tags, tags_to_keep = partition_by_keep_n(tags)
-
- cache_tags(tags_to_keep)
-
- tags
- end
-
- def filter_by_older_than(tags)
- tags, tags_to_keep = partition_by_older_than(tags)
-
- cache_tags(tags_to_keep)
-
- tags
+ def cleanup_tags_service_class
+ log_data = {
+ container_repository_id: container_repository.id,
+ container_repository_path: container_repository.path,
+ project_id: project.id
+ }
+
+ if use_gitlab_service?
+ log_info(log_data.merge(gitlab_cleanup_tags_service: true))
+ ::Projects::ContainerRepository::Gitlab::CleanupTagsService
+ else
+ log_info(log_data.merge(third_party_cleanup_tags_service: true))
+ ::Projects::ContainerRepository::ThirdParty::CleanupTagsService
+ end
end
- def pushed_at(tag)
- tag.created_at
+ def use_gitlab_service?
+ container_repository.migrated? &&
+ container_repository.gitlab_api_client.supports_gitlab_api?
end
- def truncate(tags)
- @counts[:before_truncate_size] = tags.size
- @counts[:after_truncate_size] = tags.size
-
- return tags if max_list_size == 0
-
- # truncate the list to make sure that after the #filter_keep_n
- # execution, the resulting list will be max_list_size
- truncated_size = max_list_size + keep_n_as_integer
-
- return tags if tags.size <= truncated_size
+ def can_destroy?
+ return true if container_expiration_policy
- tags = tags.sample(truncated_size)
- @counts[:after_truncate_size] = tags.size
- tags
+ can?(current_user, :destroy_container_image, project)
end
- def populate_from_cache(tags)
- @counts[:cached_tags_count] = cache.populate(tags) if caching_enabled?
- end
-
- def cache_tags(tags)
- cache.insert(tags, older_than_in_seconds) if caching_enabled?
- end
-
- def cache
- strong_memoize(:cache) do
- ::Gitlab::ContainerRepository::Tags::Cache.new(container_repository)
+ def valid_regex?
+ %w[name_regex_delete name_regex name_regex_keep].each do |param_name|
+ regex = params[param_name]
+ ::Gitlab::UntrustedRegexp.new(regex) unless regex.blank?
end
+ true
+ rescue RegexpError => e
+ ::Gitlab::ErrorTracking.log_exception(e, project_id: project.id)
+ false
end
- def caching_enabled?
- result = ::Gitlab::CurrentSettings.current_application_settings.container_registry_expiration_policies_caching &&
- container_expiration_policy &&
- older_than.present?
- !!result
- end
-
- def max_list_size
- ::Gitlab::CurrentSettings.current_application_settings.container_registry_cleanup_tags_service_max_list_size.to_i
+ def container_expiration_policy
+ params['container_expiration_policy']
end
end
end
diff --git a/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb b/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb
index 81bb94c867a..e947e9575e2 100644
--- a/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb
@@ -14,9 +14,6 @@ module Projects
end
def execute
- return error('access denied') unless can_destroy?
- return error('invalid regex') unless valid_regex?
-
with_timeout do |start_time, result|
container_repository.each_tags_page(page_size: TAGS_PAGE_SIZE) do |tags|
execute_for_tags(tags, result)
diff --git a/app/services/projects/container_repository/third_party/cleanup_tags_service.rb b/app/services/projects/container_repository/third_party/cleanup_tags_service.rb
new file mode 100644
index 00000000000..c6335629b52
--- /dev/null
+++ b/app/services/projects/container_repository/third_party/cleanup_tags_service.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+module Projects
+ module ContainerRepository
+ module ThirdParty
+ class CleanupTagsService < CleanupTagsBaseService
+ def initialize(container_repository:, current_user: nil, params: {})
+ super
+
+ @params = params.dup
+ @counts = { cached_tags_count: 0 }
+ end
+
+ def execute
+ tags = container_repository.tags
+ @counts[:original_size] = tags.size
+
+ filter_out_latest!(tags)
+ filter_by_name!(tags)
+
+ tags = truncate(tags)
+ populate_from_cache(tags)
+
+ tags = filter_keep_n(tags)
+ tags = filter_by_older_than(tags)
+
+ @counts[:before_delete_size] = tags.size
+
+ delete_tags(tags).merge(@counts).tap do |result|
+ result[:deleted_size] = result[:deleted]&.size
+
+ result[:status] = :error if @counts[:before_truncate_size] != @counts[:after_truncate_size]
+ end
+ end
+
+ private
+
+ def filter_keep_n(tags)
+ tags, tags_to_keep = partition_by_keep_n(tags)
+
+ cache_tags(tags_to_keep)
+
+ tags
+ end
+
+ def filter_by_older_than(tags)
+ tags, tags_to_keep = partition_by_older_than(tags)
+
+ cache_tags(tags_to_keep)
+
+ tags
+ end
+
+ def pushed_at(tag)
+ tag.created_at
+ end
+
+ def truncate(tags)
+ @counts[:before_truncate_size] = tags.size
+ @counts[:after_truncate_size] = tags.size
+
+ return tags if max_list_size == 0
+
+ # truncate the list to make sure that after the #filter_keep_n
+ # execution, the resulting list will be max_list_size
+ truncated_size = max_list_size + keep_n_as_integer
+
+ return tags if tags.size <= truncated_size
+
+ tags = tags.sample(truncated_size)
+ @counts[:after_truncate_size] = tags.size
+ tags
+ end
+
+ def populate_from_cache(tags)
+ @counts[:cached_tags_count] = cache.populate(tags) if caching_enabled?
+ end
+
+ def cache_tags(tags)
+ cache.insert(tags, older_than_in_seconds) if caching_enabled?
+ end
+
+ def cache
+ strong_memoize(:cache) do
+ ::Gitlab::ContainerRepository::Tags::Cache.new(container_repository)
+ end
+ end
+
+ def caching_enabled?
+ result = current_application_settings.container_registry_expiration_policies_caching &&
+ container_expiration_policy &&
+ older_than.present?
+ !!result
+ end
+
+ def max_list_size
+ current_application_settings.container_registry_cleanup_tags_service_max_list_size.to_i
+ end
+
+ def current_application_settings
+ ::Gitlab::CurrentSettings.current_application_settings
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index f1525ed9763..4e883f682fb 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -134,7 +134,7 @@ module Projects
destroy_ci_records!
destroy_mr_diff_relations!
- destroy_merge_request_diffs! if ::Feature.enabled?(:extract_mr_diff_deletions)
+ destroy_merge_request_diffs!
# Rails attempts to load all related records into memory before
# destroying: https://github.com/rails/rails/issues/22510
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 4979af6dfe1..de7ede4eabf 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -64,7 +64,11 @@ module Projects
def add_repository_to_project
if project.external_import? && !unknown_url?
begin
- Gitlab::UrlBlocker.validate!(project.import_url, ports: Project::VALID_IMPORT_PORTS)
+ Gitlab::UrlBlocker.validate!(
+ project.import_url,
+ schemes: Project::VALID_IMPORT_PROTOCOLS,
+ ports: Project::VALID_IMPORT_PORTS
+ )
rescue Gitlab::UrlBlocker::BlockedUrlError => e
raise e, s_("ImportProjects|Blocked import URL: %{message}") % { message: e.message }
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index d757b0700b9..f9a2c825608 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -122,7 +122,7 @@ module Projects
update_pending_builds if runners_settings_toggled?
- publish_event
+ publish_events
end
def after_rename_service(project)
@@ -212,7 +212,13 @@ module Projects
end
end
- def publish_event
+ def publish_events
+ publish_project_archived_event
+ publish_project_attributed_changed_event
+ publish_project_features_changed_event
+ end
+
+ def publish_project_archived_event
return unless project.archived_previously_changed?
event = Projects::ProjectArchivedEvent.new(data: {
@@ -223,6 +229,36 @@ module Projects
Gitlab::EventStore.publish(event)
end
+
+ def publish_project_attributed_changed_event
+ changes = @project.previous_changes
+
+ return if changes.blank?
+
+ event = Projects::ProjectAttributesChangedEvent.new(data: {
+ project_id: @project.id,
+ namespace_id: @project.namespace_id,
+ root_namespace_id: @project.root_namespace.id,
+ attributes: changes.keys
+ })
+
+ Gitlab::EventStore.publish(event)
+ end
+
+ def publish_project_features_changed_event
+ changes = @project.project_feature.previous_changes
+
+ return if changes.blank?
+
+ event = Projects::ProjectFeaturesChangedEvent.new(data: {
+ project_id: @project.id,
+ namespace_id: @project.namespace_id,
+ root_namespace_id: @project.root_namespace.id,
+ features: changes.keys
+ })
+
+ Gitlab::EventStore.publish(event)
+ end
end
end
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index b7df201824a..01dd6323d94 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -3,10 +3,10 @@
module Releases
class CreateService < Releases::BaseService
def execute
- return error('Access Denied', 403) unless allowed?
- return error('You are not allowed to create this tag as it is protected.', 403) unless can_create_tag?
- return error('Release already exists', 409) if release
- return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
+ return error(_('Access Denied'), 403) unless allowed?
+ return error(_('You are not allowed to create this tag as it is protected.'), 403) unless can_create_tag?
+ return error(_('Release already exists'), 409) if release
+ return error(format(_("Milestone(s) not found: %{milestones}"), milestones: inexistent_milestones.join(', ')), 400) if inexistent_milestones.any? # rubocop:disable Layout/LineLength
# should be found before the creation of new tag
# because tag creation can spawn new pipeline
diff --git a/app/services/releases/destroy_service.rb b/app/services/releases/destroy_service.rb
index 8abf9308689..ff2b3a7bd18 100644
--- a/app/services/releases/destroy_service.rb
+++ b/app/services/releases/destroy_service.rb
@@ -3,8 +3,8 @@
module Releases
class DestroyService < Releases::BaseService
def execute
- return error('Release does not exist', 404) unless release
- return error('Access Denied', 403) unless allowed?
+ return error(_('Release does not exist'), 404) unless release
+ return error(_('Access Denied'), 403) unless allowed?
if release.destroy
success(tag: existing_tag, release: release)
diff --git a/app/services/releases/update_service.rb b/app/services/releases/update_service.rb
index 2e0a2f8488a..b9b2aba9805 100644
--- a/app/services/releases/update_service.rb
+++ b/app/services/releases/update_service.rb
@@ -31,11 +31,11 @@ module Releases
private
def validate
- return error('Tag does not exist', 404) unless existing_tag
- return error('Release does not exist', 404) unless release
- return error('Access Denied', 403) unless allowed?
- return error('params is empty', 400) if empty_params?
- return error("Milestone(s) not found: #{inexistent_milestones.join(', ')}", 400) if inexistent_milestones.any?
+ return error(_('Tag does not exist'), 404) unless existing_tag
+ return error(_('Release does not exist'), 404) unless release
+ return error(_('Access Denied'), 403) unless allowed?
+ return error(_('params is empty'), 400) if empty_params?
+ return error(format(_("Milestone(s) not found: %{milestones}"), milestones: inexistent_milestones.join(', ')), 400) if inexistent_milestones.any? # rubocop:disable Layout/LineLength
end
def allowed?
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index eed03ba22fe..b8a210c0a95 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -13,7 +13,6 @@ module ResourceAccessTokens
return error("User does not have permission to create #{resource_type} access token") unless has_permission_to_create?
access_level = params[:access_level] || Gitlab::Access::MAINTAINER
- return error("Could not provision owner access to project access token") if do_not_allow_owner_access_level_for_project_bot?(access_level)
user = create_user
@@ -48,9 +47,9 @@ module ResourceAccessTokens
end
def create_user
- # Even project maintainers can create project access tokens, which in turn
+ # Even project maintainers/owners can create project access tokens, which in turn
# creates a bot user, and so it becomes necessary to have `skip_authorization: true`
- # since someone like a project maintainer does not inherently have the ability
+ # since someone like a project maintainer/owner does not inherently have the ability
# to create a new user in the system.
::Users::AuthorizedCreateService.new(current_user, default_user_params).execute
@@ -108,7 +107,7 @@ module ResourceAccessTokens
end
def create_membership(resource, user, access_level)
- resource.add_member(user, access_level, expires_at: params[:expires_at])
+ resource.add_member(user, access_level, current_user: current_user, expires_at: params[:expires_at])
end
def log_event(token)
@@ -122,12 +121,6 @@ module ResourceAccessTokens
def success(access_token)
ServiceResponse.success(payload: { access_token: access_token })
end
-
- def do_not_allow_owner_access_level_for_project_bot?(access_level)
- resource.is_a?(Project) &&
- access_level == Gitlab::Access::OWNER &&
- !current_user.can?(:manage_owners, resource)
- end
end
end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index cea7fc5769e..f38522b9764 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -102,6 +102,16 @@ class SearchService
end
end
+ def show_elasticsearch_tabs?
+ # overridden in EE
+ false
+ end
+
+ def show_epics?
+ # overridden in EE
+ false
+ end
+
private
def page
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index ddb20a835e1..0fa1bb96b13 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -106,6 +106,8 @@ module Users
def build_user_params_for_non_admin
@user_params = params.slice(*signup_params)
+ # if skip_confirmation is set to `true`, devise will set confirmed_at
+ # see: https://github.com/heartcombo/devise/blob/8593801130f2df94a50863b5db535c272b00efe1/lib/devise/models/confirmable.rb#L156
@user_params[:skip_confirmation] = skip_user_confirmation_email_from_setting if assign_skip_confirmation_from_settings?
@user_params[:name] = fallback_name if use_fallback_name?
end
diff --git a/app/services/users/dismiss_namespace_callout_service.rb b/app/services/users/dismiss_namespace_callout_service.rb
deleted file mode 100644
index 51261a93e20..00000000000
--- a/app/services/users/dismiss_namespace_callout_service.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Users
- class DismissNamespaceCalloutService < DismissCalloutService
- private
-
- def callout
- current_user.find_or_initialize_namespace_callout(params[:feature_name], params[:namespace_id])
- end
- end
-end
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index fe61335f3ed..b1ffd006795 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -62,12 +62,12 @@ module Users
# Updates the list of authorizations for the current user.
#
- # remove - The IDs of the authorization rows to remove.
+ # remove - The project IDs of the authorization rows to remove.
# add - Rows to insert in the form `[{ user_id: user_id, project_id: project_id, access_level: access_level}, ...]`
def update_authorizations(remove = [], add = [])
log_refresh_details(remove, add)
- user.remove_project_authorizations(remove) if remove.any?
+ ProjectAuthorization.delete_all_in_batches_for_user(user: user, project_ids: remove) if remove.any?
ProjectAuthorization.insert_all_in_batches(add) if add.any?
# Since we batch insert authorization rows, Rails' associations may get
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index cd2c7402713..e5e5e375198 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -194,7 +194,8 @@ class WebHookService
headers = {
'Content-Type' => 'application/json',
'User-Agent' => "GitLab/#{Gitlab::VERSION}",
- Gitlab::WebHooks::GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name)
+ Gitlab::WebHooks::GITLAB_EVENT_HEADER => self.class.hook_to_event(hook_name),
+ Gitlab::WebHooks::GITLAB_INSTANCE_HEADER => Gitlab.config.gitlab.base_url
}
headers['X-Gitlab-Token'] = Gitlab::Utils.remove_line_breaks(hook.token) if hook.token.present?
diff --git a/app/services/web_hooks/log_execution_service.rb b/app/services/web_hooks/log_execution_service.rb
index 5be8aee3ae8..1a40c877bda 100644
--- a/app/services/web_hooks/log_execution_service.rb
+++ b/app/services/web_hooks/log_execution_service.rb
@@ -17,7 +17,7 @@ module WebHooks
end
def execute
- update_hook_failure_state
+ update_hook_failure_state if WebHook.web_hooks_disable_failed?(hook)
log_execution
end
diff --git a/app/services/work_items/create_service.rb b/app/services/work_items/create_service.rb
index c2ceb701a2f..ebda043e873 100644
--- a/app/services/work_items/create_service.rb
+++ b/app/services/work_items/create_service.rb
@@ -2,7 +2,6 @@
module WorkItems
class CreateService < Issues::CreateService
- include ::Services::ReturnServiceResponses
include WidgetableService
def initialize(project:, current_user: nil, params: {}, spam_params:, widget_params: {})
@@ -17,11 +16,10 @@ module WorkItems
end
def execute
- unless @current_user.can?(:create_work_item, @project)
- return error(_('Operation not allowed'), :forbidden)
- end
+ result = super
+ return result if result.error?
- work_item = super
+ work_item = result[:issue]
if work_item.valid?
success(payload(work_item))
@@ -43,6 +41,10 @@ module WorkItems
private
+ def authorization_action
+ :create_work_item
+ end
+
def payload(work_item)
{ work_item: work_item }
end
diff --git a/app/services/work_items/update_service.rb b/app/services/work_items/update_service.rb
index 2deb8c82741..1351445f6f3 100644
--- a/app/services/work_items/update_service.rb
+++ b/app/services/work_items/update_service.rb
@@ -26,6 +26,17 @@ module WorkItems
private
+ def prepare_update_params(work_item)
+ execute_widgets(
+ work_item: work_item,
+ callback: :prepare_update_params,
+ widget_params: @widget_params,
+ service_params: params
+ )
+
+ super
+ end
+
def before_update(work_item, skip_spam_check: false)
execute_widgets(work_item: work_item, callback: :before_update_callback, widget_params: @widget_params)
@@ -38,7 +49,7 @@ module WorkItems
super
end
- def after_update(work_item)
+ def after_update(work_item, old_associations)
super
GraphqlTriggers.issuable_title_updated(work_item) if work_item.previous_changes.key?(:title)
@@ -47,5 +58,13 @@ module WorkItems
def payload(work_item)
{ work_item: work_item }
end
+
+ def handle_label_changes(issuable, old_labels)
+ return false unless super
+
+ Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_labels_changed_action(
+ author: current_user
+ )
+ end
end
end
diff --git a/app/services/work_items/widgets/base_service.rb b/app/services/work_items/widgets/base_service.rb
index 37ed2bf4b05..1ff03a09f9f 100644
--- a/app/services/work_items/widgets/base_service.rb
+++ b/app/services/work_items/widgets/base_service.rb
@@ -5,12 +5,13 @@ module WorkItems
class BaseService < ::BaseService
WidgetError = Class.new(StandardError)
- attr_reader :widget, :work_item, :current_user
+ attr_reader :widget, :work_item, :current_user, :service_params
- def initialize(widget:, current_user:)
+ def initialize(widget:, current_user:, service_params: {})
@widget = widget
@work_item = widget.work_item
@current_user = current_user
+ @service_params = service_params
end
private
diff --git a/app/services/work_items/widgets/labels_service/update_service.rb b/app/services/work_items/widgets/labels_service/update_service.rb
new file mode 100644
index 00000000000..f00ea5c95ca
--- /dev/null
+++ b/app/services/work_items/widgets/labels_service/update_service.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Widgets
+ module LabelsService
+ class UpdateService < WorkItems::Widgets::BaseService
+ def prepare_update_params(params: {})
+ return if params.blank?
+
+ service_params.merge!(params.slice(:add_label_ids, :remove_label_ids))
+ end
+ end
+ end
+ end
+end
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index 83dc1030606..b38e7d93eac 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -3,6 +3,7 @@
class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
+ include ObjectStorage::CDN::Concern
UnknownFileLocationError = Class.new(StandardError)
diff --git a/app/uploaders/object_storage/cdn.rb b/app/uploaders/object_storage/cdn.rb
index 0711ab0bd28..e49e2780147 100644
--- a/app/uploaders/object_storage/cdn.rb
+++ b/app/uploaders/object_storage/cdn.rb
@@ -10,6 +10,16 @@ module ObjectStorage
include Gitlab::Utils::StrongMemoize
+ UrlResult = Struct.new(:url, :used_cdn)
+
+ def cdn_enabled_url(project, ip_address)
+ if Feature.enabled?(:ci_job_artifacts_cdn, project) && use_cdn?(ip_address)
+ UrlResult.new(cdn_signed_url, true)
+ else
+ UrlResult.new(url, false)
+ end
+ end
+
def use_cdn?(request_ip)
return false unless cdn_options.is_a?(Hash) && cdn_options['provider']
return false unless cdn_provider
diff --git a/app/uploaders/object_storage/cdn/google_cdn.rb b/app/uploaders/object_storage/cdn/google_cdn.rb
index ea7683f131c..91bad1f8d6b 100644
--- a/app/uploaders/object_storage/cdn/google_cdn.rb
+++ b/app/uploaders/object_storage/cdn/google_cdn.rb
@@ -19,7 +19,7 @@ module ObjectStorage
ip = IPAddr.new(request_ip)
- return false if ip.private?
+ return false if ip.private? || ip.link_local? || ip.loopback?
!GoogleIpCache.google_ip?(request_ip)
end
@@ -41,7 +41,7 @@ module ObjectStorage
private
def config_valid?
- [key_name, decoded_key, cdn_url].all?(&:present?)
+ [key_name, decoded_key, cdn_url].all?(&:present?) && cdn_url.start_with?('https://')
end
def key_name
diff --git a/app/uploaders/packages/rpm/repository_file_uploader.rb b/app/uploaders/packages/rpm/repository_file_uploader.rb
new file mode 100644
index 00000000000..ff7e2bc719a
--- /dev/null
+++ b/app/uploaders/packages/rpm/repository_file_uploader.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+module Packages
+ module Rpm
+ class RepositoryFileUploader < GitlabUploader
+ include ObjectStorage::Concern
+
+ storage_options Gitlab.config.packages
+
+ after :store, :schedule_background_upload
+
+ alias_method :upload, :model
+
+ def filename
+ model.file_name
+ end
+
+ def store_dir
+ dynamic_segment
+ end
+
+ private
+
+ def dynamic_segment
+ raise ObjectNotReadyError, 'Repository file model not ready' unless model.id
+
+ Gitlab::HashedPath.new(
+ 'projects', model.project_id, 'rpm', 'repository_files', model.id,
+ root_hash: model.project_id
+ )
+ end
+ end
+ end
+end
diff --git a/app/validators/json_schemas/build_metadata_secrets.json b/app/validators/json_schemas/build_metadata_secrets.json
index 3c8035d0dcf..5dcd33a2cf0 100644
--- a/app/validators/json_schemas/build_metadata_secrets.json
+++ b/app/validators/json_schemas/build_metadata_secrets.json
@@ -24,7 +24,8 @@
},
"additionalProperties": false
},
- "^file$": { "type": "boolean" }
+ "^file$": { "type": "boolean" },
+ "^token$": { "type": "string" }
},
"additionalProperties": false
}
diff --git a/app/validators/json_schemas/ci_secure_file_metadata.json b/app/validators/json_schemas/ci_secure_file_metadata.json
new file mode 100644
index 00000000000..46a7ff60b8f
--- /dev/null
+++ b/app/validators/json_schemas/ci_secure_file_metadata.json
@@ -0,0 +1,22 @@
+{
+ "description": "CI Secure File Metadata",
+ "type": "object",
+ "properties": {
+ "id": { "type": "string" },
+ "team_name": { "type": "string" },
+ "team_id": { "type": "string" },
+ "app_name": { "type": "string" },
+ "app_id": { "type": "string" },
+ "app_id_prefix": { "type": "string" },
+ "xcode_managed": { "type": "boolean" },
+ "entitlements": { "type": "object" },
+ "devices": { "type": "array" },
+ "certificate_ids": { "type": "array" },
+ "issuer": { "type": "object" },
+ "subject": { "type": "object" }
+ },
+ "additionalProperties": true,
+ "required": [
+ "id"
+ ]
+}
diff --git a/app/validators/json_schemas/merge_request_predictions_suggested_reviewers.json b/app/validators/json_schemas/merge_request_predictions_suggested_reviewers.json
index 70112d7e414..8e80b52d9b8 100644
--- a/app/validators/json_schemas/merge_request_predictions_suggested_reviewers.json
+++ b/app/validators/json_schemas/merge_request_predictions_suggested_reviewers.json
@@ -4,7 +4,7 @@
"properties": {
"top_n": { "type": "number" },
"version": { "type": "string" },
- "changes": { "type": "array" }
+ "reviewers": { "type": "array" }
},
"additionalProperties": true
}
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index c0e42f22119..c091a2180c5 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -17,14 +17,14 @@
.form-group
= f.label :receive_max_input_size, _('Maximum push size (MB)'), class: 'label-light'
- = f.number_field :receive_max_input_size, class: 'form-control gl-form-input qa-receive-max-input-size-field', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body' }
+ = f.number_field :receive_max_input_size, class: 'form-control gl-form-input', title: _('Maximum size limit for a single commit.'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'receive_max_input_size_field' }
.form-group
= f.label :max_export_size, _('Maximum export size (MB)'), class: 'label-light'
= f.number_field :max_export_size, class: 'form-control gl-form-input', title: _('Maximum size of export files.'), data: { toggle: 'tooltip', container: 'body' }
%span.form-text.text-muted= _('Set to 0 for no size limit.')
.form-group
= f.label :max_import_size, _('Maximum import size (MB)'), class: 'label-light'
- = f.number_field :max_import_size, class: 'form-control gl-form-input qa-receive-max-import-size-field', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' }
+ = f.number_field :max_import_size, class: 'form-control gl-form-input', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' }
%span.form-text.text-muted= _('Only effective when remote storage is enabled. Set to 0 for no size limit.')
.form-group
= f.label :session_expire_delay, _('Session duration (minutes)'), class: 'label-light'
@@ -54,10 +54,10 @@
- dormant_users_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: dormant_users_help_link }
= f.gitlab_ui_checkbox_component :deactivate_dormant_users, _('Deactivate dormant users after a period of inactivity'), help_text: _('Users can reactivate their account by signing in. %{link_start}Learn more.%{link_end}').html_safe % { link_start: dormant_users_help_link_start, link_end: '</a>'.html_safe }
.form-group
- = f.label :deactivate_dormant_users_period, _('Period of inactivity (days)'), class: 'label-light'
- = f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '1'
+ = f.label :deactivate_dormant_users_period, _('Days of inactivity before deactivation'), class: 'label-light'
+ = f.number_field :deactivate_dormant_users_period, class: 'form-control gl-form-input', min: '90', step: '1'
.form-text.text-muted
- = _('Period of inactivity before deactivation.')
+ = _('Must be 90 days or more.')
.form-group
= f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light'
@@ -67,6 +67,6 @@
= f.gitlab_ui_checkbox_component :user_show_add_ssh_key_message, _("Inform users without uploaded SSH keys that they can't push over SSH until one is added")
= render 'admin/application_settings/invitation_flow_enforcement', form: f
- = render_if_exists 'admin/application_settings/updating_name_disabled_for_users', form: f
+ = render 'admin/application_settings/user_restrictions', form: f
= render_if_exists 'admin/application_settings/availability_on_namespace_setting', form: f
- = f.submit _('Save changes'), class: 'qa-save-changes-button', pajamas_button: true
+ = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 05aea2b343d..f6635ad17ef 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -53,8 +53,7 @@
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'specify-a-custom-cicd-configuration-file'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.gitlab_ui_checkbox_component :suggest_pipeline_enabled, s_('AdminSettings|Enable pipeline suggestion banner'), help_text: s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
- - if Feature.enabled?(:enforce_runner_token_expires_at)
- #js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
+ #js-runner-token-expiration-intervals{ data: runner_token_expiration_interval_attributes }
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_eks.html.haml b/app/views/admin/application_settings/_eks.html.haml
index 0bb9be497d9..62c61ad356f 100644
--- a/app/views/admin/application_settings/_eks.html.haml
+++ b/app/views/admin/application_settings/_eks.html.haml
@@ -31,4 +31,4 @@
.form-text.text-muted
= _('Only required if not using role instance credentials.')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
index fd65d4029f5..e0ff1f4be43 100644
--- a/app/views/admin/application_settings/_email.html.haml
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -21,4 +21,4 @@
.form-group
= f.gitlab_ui_checkbox_component :user_deactivation_emails_enabled, _('Enable user deactivation emails'), help_text: _('Send emails to users upon account deactivation.')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
index 7919fde631f..a5e10846488 100644
--- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml
+++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
@@ -47,4 +47,4 @@
= f.text_field :external_authorization_service_default_label, class: 'form-control gl-form-input'
%span.form-text.text-muted
= external_authorization_client_url_help_text
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_floc.html.haml b/app/views/admin/application_settings/_floc.html.haml
index e56ba635890..cb8b2d3dfcd 100644
--- a/app/views/admin/application_settings/_floc.html.haml
+++ b/app/views/admin/application_settings/_floc.html.haml
@@ -19,4 +19,4 @@
.form-group
= f.gitlab_ui_checkbox_component :floc_enabled,
s_('FloC|Participate in FLoC')
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml
index ade6dac606a..f459ff5abc4 100644
--- a/app/views/admin/application_settings/_gitaly.html.haml
+++ b/app/views/admin/application_settings/_gitaly.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-gitaly-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-gitaly-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -18,4 +18,4 @@
.form-text.text-muted
= _('Timeout for moderately fast Gitaly operations (in seconds). Provide a value between Default timeout and Fast timeout.')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml
index df534f18bde..09817a9172f 100644
--- a/app/views/admin/application_settings/_gitpod.html.haml
+++ b/app/views/admin/application_settings/_gitpod.html.haml
@@ -26,4 +26,4 @@
= s_('Gitpod|The URL to your Gitpod instance configured to read your GitLab projects, such as https://gitpod.example.com.')
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('integration/gitpod', anchor: 'enable-gitpod-in-your-user-settings') }
= s_('Gitpod|To use the integration, each user must also enable Gitpod on their GitLab account. %{link_start}How do I enable it?%{link_end} ').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml
index 21eb4caf579..11ebad07e9a 100644
--- a/app/views/admin/application_settings/_help_page.html.haml
+++ b/app/views/admin/application_settings/_help_page.html.haml
@@ -21,4 +21,4 @@
- docs_link_url = help_page_path('user/admin_area/settings/help_page', anchor: 'destination-requirements')
- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_link_url }
%span.form-text.text-muted#support_help_block= html_escape(_('Requests for pages at %{code_start}%{help_text_url}%{code_end} redirect to the URL. The destination must meet certain requirements. %{docs_link_start}Learn more.%{docs_link_end}')) % { code_start: '<code>'.html_safe, help_text_url: help_url, code_end: '</code>'.html_safe, docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_import_export_limits.html.haml b/app/views/admin/application_settings/_import_export_limits.html.haml
index bc4a1577f90..8cb7915f847 100644
--- a/app/views/admin/application_settings/_import_export_limits.html.haml
+++ b/app/views/admin/application_settings/_import_export_limits.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-import-export-limits-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-import-export-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -35,4 +35,4 @@
= f.label :group_download_export_limit, _('Maximum group export download requests per minute'), class: 'label-bold'
= f.number_field :group_download_export_limit, class: 'form-control gl-form-input'
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml
index 4362ae9cb9b..01d7bf0af67 100644
--- a/app/views/admin/application_settings/_ip_limits.html.haml
+++ b/app/views/admin/application_settings/_ip_limits.html.haml
@@ -66,4 +66,4 @@
.form-text.text-muted
= html_escape(_("If blank, defaults to %{code_open}Retry later%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_issue_limits.html.haml b/app/views/admin/application_settings/_issue_limits.html.haml
index 431e2a64c46..147aab443b2 100644
--- a/app/views/admin/application_settings/_issue_limits.html.haml
+++ b/app/views/admin/application_settings/_issue_limits.html.haml
@@ -6,4 +6,4 @@
= f.label :issues_create_limit, _('Maximum number of requests per minute')
= f.number_field :issues_create_limit, class: 'form-control gl-form-input'
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_jira_connect_application_key.html.haml b/app/views/admin/application_settings/_jira_connect_application_key.html.haml
index e3df408cd4c..b67e7680720 100644
--- a/app/views/admin/application_settings/_jira_connect_application_key.html.haml
+++ b/app/views/admin/application_settings/_jira_connect_application_key.html.haml
@@ -18,4 +18,4 @@
.form-group
= f.label :jira_connect_application_key, s_('JiraConnect|Jira Connect Application ID'), class: 'label-bold'
= f.text_field :jira_connect_application_key, class: 'form-control gl-form-input'
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml
index a6ed48ef4fe..90cb34395d8 100644
--- a/app/views/admin/application_settings/_localization.html.haml
+++ b/app/views/admin/application_settings/_localization.html.haml
@@ -15,5 +15,5 @@
- time_tracking_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: time_tracking_help_link }
= f.gitlab_ui_checkbox_component :time_tracking_limit_to_hours, _('Limit display of time tracking units to hours.'), help_text: _('Display time tracking in issues in total hours only. %{link_start}What is time tracking?%{link_end}').html_safe % { link_start: time_tracking_help_link_start, link_end: '</a>'.html_safe }
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_network_rate_limits.html.haml b/app/views/admin/application_settings/_network_rate_limits.html.haml
index f1857a9749a..300180f7b9a 100644
--- a/app/views/admin/application_settings/_network_rate_limits.html.haml
+++ b/app/views/admin/application_settings/_network_rate_limits.html.haml
@@ -30,4 +30,4 @@
= f.label :"throttle_authenticated_#{setting_fragment}_period_in_seconds", _('Authenticated API rate limit period in seconds'), class: 'label-bold'
= f.number_field :"throttle_authenticated_#{setting_fragment}_period_in_seconds", class: 'form-control gl-form-input'
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_note_limits.html.haml b/app/views/admin/application_settings/_note_limits.html.haml
index 40760b3c45e..99cf0ebc669 100644
--- a/app/views/admin/application_settings/_note_limits.html.haml
+++ b/app/views/admin/application_settings/_note_limits.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-note-limits-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-note-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -12,4 +12,4 @@
= _('List of users who are allowed to exceed the rate limit. Example: username1, username2')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index bacfe056683..3505a3bf3ee 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -22,4 +22,4 @@
s_('OutboundRequests|Enforce DNS rebinding attack protection'),
help_text: _('OutboundRequests|Resolve IP addresses once and uses them to submit requests.')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_package_registry.html.haml b/app/views/admin/application_settings/_package_registry.html.haml
index 4bdfa5bfe83..3506038ca68 100644
--- a/app/views/admin/application_settings/_package_registry.html.haml
+++ b/app/views/admin/application_settings/_package_registry.html.haml
@@ -20,12 +20,12 @@
%ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs.mb-3
- @plans.each_with_index do |plan, index|
%li
- = link_to admin_plan_limits_path(anchor: 'js-package-settings'), data: { target: "div#plan#{index}", action: "plan#{index}", toggle: 'tab'}, class: index == 0 ? 'active': '' do
+ = link_to admin_plan_limits_path(anchor: 'js-package-settings'), data: { target: "div#plan-package#{index}", action: "plan#{index}", toggle: 'tab'}, class: index == 0 ? 'active': '' do
= plan.name.capitalize
.tab-content
- @plans.each_with_index do |plan, index|
- .tab-pane{ :id => "plan#{index}", class: index == 0 ? 'active': '' }
- = form_for plan.actual_limits, url: admin_plan_limits_path(anchor: 'js-package-settings'), html: { class: 'fieldset-form' }, method: :post do |f|
+ .tab-pane{ :id => "plan-package#{index}", class: index == 0 ? 'active': '' }
+ = gitlab_ui_form_for plan.actual_limits, url: admin_plan_limits_path(anchor: 'js-package-settings'), html: { class: 'fieldset-form' }, method: :post do |f|
= form_errors(plan)
%fieldset
= f.hidden_field(:plan_id, value: plan.id)
@@ -53,4 +53,4 @@
.form-group
= f.label :generic_packages_max_file_size, _('Generic package file size in bytes'), class: 'label-bold'
= f.number_field :generic_packages_max_file_size, class: 'form-control gl-form-input'
- = f.submit _('Save %{name} size limits').html_safe % { name: plan.name.capitalize }, class: 'btn gl-button btn-confirm'
+ = f.submit _('Save %{name} size limits').html_safe % { name: plan.name.capitalize }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index cf43d3ddeca..97d9426581e 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -46,4 +46,4 @@
= f.gitlab_ui_checkbox_component :lets_encrypt_terms_of_service_accepted,
s_("AdminSettings|I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end} (PDF).").html_safe % { link_start: terms_of_service_link_start, link_end: '</a>'.html_safe }
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index e0ba8d93fbd..86a01e1785e 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -23,4 +23,4 @@
.form-text.text-muted
= _('Threshold number of changes (branches or tags) in a single push above which a bulk push event is created (default is 3).')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index 4e37c4c3c98..d4f6d84ea74 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -10,4 +10,4 @@
= f.label :performance_bar_allowed_group_path, _('Allow access to members of the following group'), class: 'label-bold'
= f.text_field :performance_bar_allowed_group_path, class: 'form-control gl-form-input', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm qa-save-changes-button'
+ = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_pipeline_limits.html.haml b/app/views/admin/application_settings/_pipeline_limits.html.haml
index e93823172db..b7dffe63777 100644
--- a/app/views/admin/application_settings/_pipeline_limits.html.haml
+++ b/app/views/admin/application_settings/_pipeline_limits.html.haml
@@ -6,4 +6,4 @@
= f.label :pipeline_limit_per_project_user_sha, _('Maximum number of requests per minute')
= f.number_field :pipeline_limit_per_project_user_sha, class: 'form-control gl-form-input'
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml
index 982531e9a2f..3db1272c77b 100644
--- a/app/views/admin/application_settings/_prometheus.html.haml
+++ b/app/views/admin/application_settings/_prometheus.html.haml
@@ -18,4 +18,4 @@
.form-text.text-muted
Only track method calls that take longer to complete than the given duration.
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_protected_paths.html.haml b/app/views/admin/application_settings/_protected_paths.html.haml
index 1f3f67c71c7..3a7a951d137 100644
--- a/app/views/admin/application_settings/_protected_paths.html.haml
+++ b/app/views/admin/application_settings/_protected_paths.html.haml
@@ -21,4 +21,4 @@
- relative_url_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: relative_url_link }
= _('All paths are relative to the GitLab URL. Do not include %{relative_url_link_start}relative URLs%{relative_url_link_end}.').html_safe % { relative_url_link_start: relative_url_link_start, relative_url_link_end: '</a>'.html_safe }
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
index 856db32e088..6a8ef86a56e 100644
--- a/app/views/admin/application_settings/_registry.html.haml
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -34,4 +34,4 @@
= f.gitlab_ui_checkbox_component :container_registry_expiration_policies_caching, _("Enable container expiration caching."),
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
index dad8d5f3fae..869f26ceb10 100644
--- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -9,4 +9,4 @@
= render_if_exists 'admin/application_settings/mirror_settings', form: f
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index 9e7f2812d64..12dd8816783 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -27,4 +27,4 @@
= storage_form.text_field storage, class: 'form-text-input'
= storage_form.label storage, storage, class: 'label-bold form-check-label'
%br
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_sentry.html.haml b/app/views/admin/application_settings/_sentry.html.haml
index cfd34f6ca15..20164cfe88d 100644
--- a/app/views/admin/application_settings/_sentry.html.haml
+++ b/app/views/admin/application_settings/_sentry.html.haml
@@ -17,4 +17,4 @@
= f.label :sentry_environment, _('Environment'), class: 'label-light'
= f.text_field :sentry_environment, class: 'form-control gl-form-input', placeholder: Rails.env
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_sidekiq_job_limits.html.haml b/app/views/admin/application_settings/_sidekiq_job_limits.html.haml
index eaf4bbf4702..068a8155450 100644
--- a/app/views/admin/application_settings/_sidekiq_job_limits.html.haml
+++ b/app/views/admin/application_settings/_sidekiq_job_limits.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-sidekiq-job-limits-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-sidekiq-job-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -18,4 +18,4 @@
.form-text.text-muted
= _("Threshold in bytes at which to reject Sidekiq jobs. Set this to 0 to if you don't want to limit Sidekiq jobs.")
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 48f0b9b2c31..3e2551d753a 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -55,4 +55,4 @@
= f.label :sign_in_text, _('Sign-in text'), class: 'label-bold'
= f.text_area :sign_in_text, class: 'form-control gl-form-input', rows: 4
%span.form-text.text-muted#home_help_block= _("Add text to the sign-in page. Markdown enabled.")
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml
index 8684b909853..4e7d9b8ab21 100644
--- a/app/views/admin/application_settings/_snowplow.html.haml
+++ b/app/views/admin/application_settings/_snowplow.html.haml
@@ -31,4 +31,4 @@
.form-text.text-muted
= _('The Snowplow cookie domain.')
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_sourcegraph.html.haml b/app/views/admin/application_settings/_sourcegraph.html.haml
index 9e99b496ad0..b56ca12baec 100644
--- a/app/views/admin/application_settings/_sourcegraph.html.haml
+++ b/app/views/admin/application_settings/_sourcegraph.html.haml
@@ -29,4 +29,4 @@
= f.text_field :sourcegraph_url, class: 'form-control gl-form-input', placeholder: s_('SourcegraphAdmin|https://sourcegraph.example.com')
.form-text.text-muted
= s_('SourcegraphAdmin|Configure the URL to a Sourcegraph instance which can read your GitLab projects.')
- = f.submit s_('SourcegraphAdmin|Save changes'), class: 'gl-button btn btn-confirm'
+ = f.submit s_('SourcegraphAdmin|Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
index a4b6e061c43..8da441d5245 100644
--- a/app/views/admin/application_settings/_terms.html.haml
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -11,4 +11,4 @@
.form-text.text-muted
= _("Markdown supported.")
= link_to _('What is Markdown?'), help_page_path('user/markdown.md'), target: '_blank', rel: 'noopener noreferrer'
- = f.submit _("Save changes"), class: "gl-button btn btn-confirm"
+ = f.submit _("Save changes"), pajamas_button: true
diff --git a/app/views/admin/application_settings/_third_party_offers.html.haml b/app/views/admin/application_settings/_third_party_offers.html.haml
index 20a60ac870a..ed809c6db52 100644
--- a/app/views/admin/application_settings/_third_party_offers.html.haml
+++ b/app/views/admin/application_settings/_third_party_offers.html.haml
@@ -16,4 +16,4 @@
= f.gitlab_ui_checkbox_component :hide_third_party_offers,
_('Do not display content for customer experience improvement and offers from third parties')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index 046b59dbd18..2eda3eab8c7 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -50,4 +50,4 @@
%li
= s_('AdminSettings|Restrict group access by IP address. %{link_start}Learn more%{link_end}.').html_safe % { link_start: restrict_ip_link, link_end: link_end }
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_user_restrictions.html.haml b/app/views/admin/application_settings/_user_restrictions.html.haml
new file mode 100644
index 00000000000..de8faa6705f
--- /dev/null
+++ b/app/views/admin/application_settings/_user_restrictions.html.haml
@@ -0,0 +1,6 @@
+- form = local_assigns.fetch(:form)
+
+.form-group
+ = label_tag _('User restrictions')
+ = render_if_exists 'admin/application_settings/updating_name_disabled_for_users', form: form
+ = form.gitlab_ui_checkbox_component :can_create_group, _("Allow users to create top-level groups")
diff --git a/app/views/admin/application_settings/_users_api_limits.html.haml b/app/views/admin/application_settings/_users_api_limits.html.haml
index 3918c76b12c..ca6f1113c4a 100644
--- a/app/views/admin/application_settings/_users_api_limits.html.haml
+++ b/app/views/admin/application_settings/_users_api_limits.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-users-api-limits-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-users-api-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -11,4 +11,4 @@
.form-text.text-muted{ id: 'users-api-limit-users-allowlist-field-description' }
= _('List of users who are allowed to exceed the rate limit. Example: username1, username2')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_whats_new.html.haml b/app/views/admin/application_settings/_whats_new.html.haml
index 3248969ca16..986402ad5f1 100644
--- a/app/views/admin/application_settings/_whats_new.html.haml
+++ b/app/views/admin/application_settings/_whats_new.html.haml
@@ -5,4 +5,4 @@
.gl-mb-4
= f.gitlab_ui_radio_component :whats_new_variant, variant, whats_new_variants_label(variant), help_text: whats_new_variants_description(variant)
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/appearances/preview_sign_in.html.haml b/app/views/admin/application_settings/appearances/preview_sign_in.html.haml
index 2e4ab714048..1c2350e2835 100644
--- a/app/views/admin/application_settings/appearances/preview_sign_in.html.haml
+++ b/app/views/admin/application_settings/appearances/preview_sign_in.html.haml
@@ -9,5 +9,5 @@
= label_tag :password
= password_field_tag :password, nil, disabled: true, class: "form-control gl-form-input bottom", title: title
.form-group
- = button_tag _("Sign in"), disabled: true, class: "btn gl-button btn-confirm", type: "button", title: title
-
+ = render Pajamas::ButtonComponent.new(variant: :confirm, disabled: true, button_options: { title: title }) do
+ = _("Sign in")
diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml
index f0f7e6868da..b7244c45871 100644
--- a/app/views/admin/application_settings/ci_cd.html.haml
+++ b/app/views/admin/application_settings/ci_cd.html.haml
@@ -10,7 +10,7 @@
%p.settings-message.text-center
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/index', anchor: 'protected-cicd-variables') }
= s_('Environment variables on this GitLab instance are configured to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- #js-instance-variables{ data: { endpoint: admin_ci_variables_path, group: 'true', maskable_regex: ci_variable_maskable_regex, protected_by_default: ci_variable_protected_by_default?.to_s} }
+ #js-instance-variables{ data: { endpoint: admin_ci_variables_path, maskable_regex: ci_variable_maskable_regex, protected_by_default: ci_variable_protected_by_default?.to_s} }
%section.settings.as-ci-cd.no-animate#js-ci-cd-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
@@ -38,12 +38,11 @@
.settings-content
= render 'registry'
-- if Feature.enabled?(:runner_registration_control)
- %section.settings.as-runner.no-animate#js-runner-settings{ class: ('expanded' if expanded_by_default?) }
- .settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
- = s_('Runners|Runner registration')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
- = expanded_by_default? ? 'Collapse' : 'Expand'
- .settings-content
- = render 'runner_registrars_form'
+%section.settings.as-runner.no-animate#js-runner-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = s_('Runners|Runner registration')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded_by_default? ? 'Collapse' : 'Expand'
+ .settings-content
+ = render 'runner_registrars_form'
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index cd63873a893..ec5d1ef4a34 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -24,6 +24,8 @@
.settings-content
= render 'account_and_limit'
+= render_if_exists 'admin/application_settings/free_user_cap'
+
%section.settings.as-diff-limits.no-animate#js-merge-request-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
@@ -102,7 +104,7 @@
= f.gitlab_ui_checkbox_component :web_ide_clientside_preview_enabled,
s_('IDE|Live Preview'),
help_text: s_('Preview JavaScript projects in the Web IDE with CodeSandbox Live Preview. %{link_start}Learn more.%{link_end} ').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
= render_if_exists 'admin/application_settings/maintenance_mode_settings_form'
= render 'admin/application_settings/gitpod'
diff --git a/app/views/admin/application_settings/service_usage_data.html.haml b/app/views/admin/application_settings/service_usage_data.html.haml
index 25c8bd12345..06bb9df84c4 100644
--- a/app/views/admin/application_settings/service_usage_data.html.haml
+++ b/app/views/admin/application_settings/service_usage_data.html.haml
@@ -5,25 +5,26 @@
- @content_class = "limit-container-width" unless fluid_layout
- payload_class = 'js-service-ping-payload'
-%h3= name
+%section.js-search-settings-section
+ %h3= name
-- if @service_ping_data_present
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } } ) do
- = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
- %span.js-text.gl-display-inline= _('Preview payload')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } } ) do
- = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
- %span.js-text.gl-display-inline= _('Download payload')
- %pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
-- else
- = render Pajamas::AlertComponent.new(variant: :warning,
- dismissible: false,
- title: _('Service Ping payload not found in the application cache')) do |c|
+ - if @service_ping_data_present
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } } ) do
+ = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
+ %span.js-text.gl-display-inline= _('Preview payload')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } } ) do
+ = gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
+ %span.js-text.gl-display-inline= _('Download payload')
+ %pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
+ - else
+ = render Pajamas::AlertComponent.new(variant: :warning,
+ dismissible: false,
+ title: _('Service Ping payload not found in the application cache')) do |c|
- = c.body do
- - enable_service_ping_link_url = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'enable-or-disable-usage-statistics')
- - enable_service_ping_link = '<a href="%{url}">'.html_safe % { url: enable_service_ping_link_url }
- - generate_manually_link_url = help_page_path('administration/troubleshooting/gitlab_rails_cheat_sheet', anchor: 'generate-service-ping')
- - generate_manually_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: generate_manually_link_url }
+ = c.body do
+ - enable_service_ping_link_url = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'enable-or-disable-usage-statistics')
+ - enable_service_ping_link = '<a href="%{url}">'.html_safe % { url: enable_service_ping_link_url }
+ - generate_manually_link_url = help_page_path('administration/troubleshooting/gitlab_rails_cheat_sheet', anchor: 'generate-service-ping')
+ - generate_manually_link = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: generate_manually_link_url }
- = html_escape(s_('%{enable_service_ping_link_start}Enable%{link_end} or %{generate_manually_link_start}generate%{link_end} Service Ping to preview and download service usage data payload.')) % { enable_service_ping_link_start: enable_service_ping_link, generate_manually_link_start: generate_manually_link, link_end: '</a>'.html_safe }
+ = html_escape(s_('%{enable_service_ping_link_start}Enable%{link_end} or %{generate_manually_link_start}generate%{link_end} Service Ping to preview and download service usage data payload.')) % { enable_service_ping_link_start: enable_service_ping_link, generate_manually_link_start: generate_manually_link, link_end: '</a>'.html_safe }
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index fd73d4c5671..83347034cc5 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -36,5 +36,5 @@
= render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes, f: f
.form-actions
- = f.submit _('Save application'), class: "gl-button btn btn-confirm wide"
+ = f.submit _('Save application'), pajamas_button: true
= link_to _('Cancel'), admin_applications_path, class: "gl-button btn btn-default btn-cancel"
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 6d2cab06010..15ce9b692f0 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -7,5 +7,4 @@
= html_escape(_('GitLab uses %{linkStart}Sidekiq%{linkEnd} to process background jobs')) % { linkStart: sidekiq_link_start, linkEnd: '</a>'.html_safe }
%hr
-.card.gl-rounded-0
- %iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" }
+%iframe{ src: sidekiq_path, width: '100%', height: 970, style: "border: 0" }
diff --git a/app/views/admin/broadcast_messages/_table.html.haml b/app/views/admin/broadcast_messages/_table.html.haml
new file mode 100644
index 00000000000..c5cd333f9dd
--- /dev/null
+++ b/app/views/admin/broadcast_messages/_table.html.haml
@@ -0,0 +1,38 @@
+- targeted_broadcast_messages_enabled = Feature.enabled?(:role_targeted_broadcast_messages)
+
+- if @broadcast_messages.any?
+ .table-responsive
+ %table.table.b-table.gl-table
+ %thead
+ %tr
+ %th= _('Status')
+ %th= _('Preview')
+ %th= _('Starts')
+ %th= _('Ends')
+ - if targeted_broadcast_messages_enabled
+ %th= _('Target roles')
+ %th= _('Target Path')
+ %th= _('Type')
+ %th &nbsp;
+ %tbody
+ - @broadcast_messages.each do |message|
+ %tr
+ %td
+ = broadcast_message_status(message)
+ %td
+ = broadcast_message(message, preview: true)
+ %td
+ = message.starts_at
+ %td
+ = message.ends_at
+ - if targeted_broadcast_messages_enabled
+ %td
+ = target_access_levels_display(message.target_access_levels)
+ %td
+ = message.target_path
+ %td
+ = message.broadcast_type.capitalize
+ %td.gl-white-space-nowrap<
+ = link_to sprite_icon('pencil', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: _('Edit'), class: 'btn btn-icon gl-button'
+ = link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: _('Remove'), class: 'js-remove-tr btn btn-icon gl-button btn-danger gl-ml-3'
+ = paginate @broadcast_messages, theme: 'gitlab'
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index 46924393a27..7559365e49a 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -1,49 +1,30 @@
- breadcrumb_title _("Messages")
- page_title _("Broadcast Messages")
-- targeted_broadcast_messages_enabled = Feature.enabled?(:role_targeted_broadcast_messages)
+- vue_app_enabled = Feature.enabled?(:vue_broadcast_messages, current_user)
%h1.page-title.gl-font-size-h-display
= _('Broadcast Messages')
%p.light
= _('Use banners and notifications to notify your users about scheduled maintenance, recent upgrades, and more.')
-= render 'form'
-
-%br.clearfix
-
-- if @broadcast_messages.any?
- .table-responsive
- %table.table.b-table.gl-table
- %thead
- %tr
- %th= _('Status')
- %th= _('Preview')
- %th= _('Starts')
- %th= _('Ends')
- - if targeted_broadcast_messages_enabled
- %th= _('Target roles')
- %th= _('Target Path')
- %th= _('Type')
- %th &nbsp;
- %tbody
- - @broadcast_messages.each do |message|
- %tr
- %td
- = broadcast_message_status(message)
- %td
- = broadcast_message(message, preview: true)
- %td
- = message.starts_at
- %td
- = message.ends_at
- - if targeted_broadcast_messages_enabled
- %td
- = target_access_levels_display(message.target_access_levels)
- %td
- = message.target_path
- %td
- = message.broadcast_type.capitalize
- %td.gl-white-space-nowrap<
- = link_to sprite_icon('pencil', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: _('Edit'), class: 'btn btn-icon gl-button'
- = link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: _('Remove'), class: 'js-remove-tr btn btn-icon gl-button btn-danger gl-ml-3'
- = paginate @broadcast_messages, theme: 'gitlab'
+- if vue_app_enabled
+ #js-broadcast-messages{ data: {
+ page: params[:page] || 1,
+ messages_count: @broadcast_messages.total_count,
+ messages: @broadcast_messages.map { |message| {
+ id: message.id,
+ status: broadcast_message_status(message),
+ preview: broadcast_message(message, preview: true),
+ starts_at: message.starts_at.to_s,
+ ends_at: message.ends_at.to_s,
+ target_roles: target_access_levels_display(message.target_access_levels),
+ target_path: message.target_path,
+ type: message.broadcast_type.capitalize,
+ edit_path: edit_admin_broadcast_message_path(message),
+ delete_path: admin_broadcast_message_path(message) + '.js'
+ } }.to_json
+ } }
+- else
+ = render 'form'
+ %br.clearfix
+ = render 'table'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 271f89a6b08..ccea1714973 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -35,7 +35,7 @@
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-default")
= c.footer do
.d-flex.align-items-center
- = link_to(s_('AdminArea|View latest projects'), admin_projects_path)
+ = link_to(s_('AdminArea|View latest projects'), admin_projects_path(sort: 'created_desc'))
= sprite_icon('chevron-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
.col-md-4.gl-mb-6
= render Pajamas::CardComponent.new(**component_params) do |c|
@@ -71,7 +71,7 @@
= link_to(s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-default")
= c.footer do
.d-flex.align-items-center
- = link_to(s_('AdminArea|View latest groups'), admin_groups_path)
+ = link_to(s_('AdminArea|View latest groups'), admin_groups_path(sort: 'created_desc'))
= sprite_icon('chevron-right', size: 12, css_class: 'gl-text-gray-700 gl-ml-2')
.row
.col-md-4.gl-mb-6
diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml
index 12a1c0c3de2..acdf503727d 100644
--- a/app/views/admin/deploy_keys/edit.html.haml
+++ b/app/views/admin/deploy_keys/edit.html.haml
@@ -3,8 +3,8 @@
%hr
%div
- = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
+ = gitlab_ui_form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
- = f.submit _('Save changes'), class: 'btn gl-button btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
= link_to _('Cancel'), admin_deploy_keys_path, class: 'btn gl-button btn-default btn-cancel'
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
index 74882900756..a03d6cb5a94 100644
--- a/app/views/admin/deploy_keys/new.html.haml
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -3,8 +3,9 @@
%hr
%div
- = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
+ = gitlab_ui_form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
- = f.submit 'Create', class: 'btn gl-button btn-confirm', data: { qa_selector: "add_deploy_key_button" }
- = link_to _('Cancel'), admin_deploy_keys_path, class: 'btn gl-button btn-default btn-cancel'
+ = f.submit 'Create', data: { qa_selector: "add_deploy_key_button" }, pajamas_button: true
+ = render Pajamas::ButtonComponent.new(href: admin_deploy_keys_path) do
+ = _('Cancel')
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 69e9e4260b4..7adba0d023b 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -35,10 +35,10 @@
= c.body do
= render 'shared/group_tips'
.gl-mt-5
- = f.submit _('Create group'), class: "gl-button btn btn-confirm"
+ = f.submit _('Create group'), pajamas_button: true
= link_to _('Cancel'), admin_groups_path, class: "gl-button btn btn-default btn-cancel"
- else
.gl-mt-5
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
= link_to _('Cancel'), admin_group_path(@group), class: "gl-button btn btn-cancel"
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index c27ff348f59..a1afb1ddbfa 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -1,7 +1,6 @@
- group = local_assigns.fetch(:group)
-- css_class = "gl-display-flex!#{' no-description' if group.description.blank?}"
-%li.group-row.gl-py-3.gl-align-items-center{ class: css_class, data: { qa_selector: 'group_row_content' } }
+%li.group-row.gl-py-3.gl-align-items-center{ class: 'gl-display-flex!', data: { qa_selector: 'group_row_content' } }
.avatar-container.rect-avatar.s40.gl-flex-shrink-0
= group_icon(group, class: "avatar s40")
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index a57d3170cbd..6d370919460 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -13,123 +13,112 @@
%hr
.row
.col-md-6
- .card
- .card-header
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-p-0' }) do |c|
+ - c.header do
= _('Group info:')
- %ul.content-list
- %li
- .avatar-container.rect-avatar.s60
- = group_icon(@group, class: "avatar s60")
- %li
- %span.light= _('Name:')
- %strong
- = link_to @group.name, group_path(@group)
- %li
- %span.light= _('Path:')
- %strong
- = @group.path
-
- %li
- %span.light= _('Description:')
- %strong
- = @group.description
-
- %li
- %span.light= _('Visibility level:')
- %strong
- = visibility_level_label(@group.visibility_level)
-
- %li
- %span.light= _('Created on:')
- %strong
- = @group.created_at.to_s(:medium)
-
- %li
- %span.light= _('ID:')
- %strong
- = @group.id
-
- = render_if_exists 'admin/namespace_plan_info', namespace: @group
-
- %li
- = render 'shared/storage_counter_statistics', storage_size: @group.storage_size, storage_details: @group
-
- %li
- %span.light= _('Group Git LFS status:')
- %strong
- = group_lfs_status(@group)
- = link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index')
-
- = render_if_exists 'namespaces/shared_runner_status', namespace: @group
- = render_if_exists 'namespaces/additional_minutes_status', namespace: @group
+ - c.body do
+ %ul.content-list.content-list-items-padding
+ %li
+ .avatar-container.rect-avatar.s60
+ = group_icon(@group, class: "avatar s60")
+ %li
+ %span.light= _('Name:')
+ %strong
+ = link_to @group.name, group_path(@group)
+ %li
+ %span.light= _('Path:')
+ %strong
+ = @group.path
+
+ %li
+ %span.light= _('Description:')
+ %strong
+ = @group.description
+
+ %li
+ %span.light= _('Visibility level:')
+ %strong
+ = visibility_level_label(@group.visibility_level)
+
+ %li
+ %span.light= _('Created on:')
+ %strong
+ = @group.created_at.to_s(:medium)
+
+ %li
+ %span.light= _('ID:')
+ %strong
+ = @group.id
+
+ = render_if_exists 'admin/namespace_plan_info', namespace: @group
+
+ %li
+ = render 'shared/storage_counter_statistics', storage_size: @group.storage_size, storage_details: @group
+
+ %li
+ %span.light= _('Group Git LFS status:')
+ %strong
+ = group_lfs_status(@group)
+ = link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index')
+
+ = render_if_exists 'namespaces/shared_runner_status', namespace: @group
+ = render_if_exists 'namespaces/additional_minutes_status', namespace: @group
= render 'shared/custom_attributes', custom_attributes: @group.custom_attributes
= render_if_exists 'ldap_group_links/ldap_group_links_show', group: @group
- .card
- .card-header
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-p-0' }) do |c|
+ - c.header do
= _('Projects')
= gl_badge_tag @group.projects.count
- %ul.content-list
- - @projects.each do |project|
- %li
- %strong
- = link_to project.full_name, [:admin, project]
- = gl_badge_tag storage_counter(project.statistics.storage_size)
- %span.float-right.light
- %span.monospace= project.full_path + '.git'
- - unless @projects.size < Kaminari.config.default_per_page
- .card-footer
- = paginate @projects, param_name: 'projects_page', theme: 'gitlab'
-
- - shared_projects = @group.shared_projects.sort_by(&:name)
- - unless shared_projects.empty?
- .card
- .card-header
- = _('Projects shared with %{group_name}') % { group_name: @group.name }
- = gl_badge_tag shared_projects.size
- %ul.content-list
- - shared_projects.each do |project|
+ - c.body do
+ %ul.content-list.content-list-items-padding
+ - @projects.each do |project|
%li
%strong
= link_to project.full_name, [:admin, project]
= gl_badge_tag storage_counter(project.statistics.storage_size)
%span.float-right.light
%span.monospace= project.full_path + '.git'
+ - unless @projects.size < Kaminari.config.default_per_page
+ - c.footer do
+ = paginate @projects, param_name: 'projects_page', theme: 'gitlab'
+
+ - shared_projects = @group.shared_projects.sort_by(&:name)
+ - unless shared_projects.empty?
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-p-0' }) do |c|
+ - c.header do
+ = _('Projects shared with %{group_name}') % { group_name: @group.name }
+ = gl_badge_tag shared_projects.size
+ - c.body do
+ %ul.content-list.content-list-items-padding
+ - shared_projects.each do |project|
+ %li
+ %strong
+ = link_to project.full_name, [:admin, project]
+ = gl_badge_tag storage_counter(project.statistics.storage_size)
+ %span.float-right.light
+ %span.monospace= project.full_path + '.git'
.col-md-6
= render 'shared/admin/admin_note'
- if can?(current_user, :admin_group_member, @group)
- .card
- .card-header
- = _('Add user(s) to the group:')
- .card-body.form-holder
- %p.light
- - help_link_open = '<strong><a href="%{help_url}">'.html_safe % { help_url: help_page_url("user/permissions") }
- = html_escape(_('Read more about project permissions %{help_link_open}here%{help_link_close}')) % { help_link_open: help_link_open, help_link_close: '</a></strong>'.html_safe }
-
- = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
- %div
- = users_select_tag(:user_id, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
- .gl-mt-3
- = select_tag :access_level, options_for_select(@group.access_level_roles), class: "project-access-select select2"
- %hr
- = button_tag _('Add users to group'), class: "gl-button btn btn-confirm"
= render 'shared/members/requests', membership_source: @group, group: @group, requesters: @requesters, force_mobile_view: true
- .card
- .card-header
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-p-0' }) do |c|
+ - c.header do
= html_escape(_("%{group_name} group members")) % { group_name: "<strong>#{html_escape(@group.name)}</strong>".html_safe }
= gl_badge_tag @group.users_count
= render 'shared/members/manage_access_button', path: group_group_members_path(@group)
- %ul.content-list.group-users-list.content-list.members-list
- = render partial: 'shared/members/member',
- collection: @members, as: :member,
- locals: { membership_source: @group,
- group: @group,
- current_user_is_group_owner: current_user_is_group_owner }
+ - c.body do
+ %ul.content-list.group-users-list.members-list
+ = render partial: 'shared/members/member',
+ collection: @members, as: :member,
+ locals: { membership_source: @group,
+ group: @group,
+ current_user_is_group_owner: current_user_is_group_owner }
- unless @members.size < Kaminari.config.default_per_page
- .card-footer
+ - c.footer do
= paginate @members, param_name: 'members_page', theme: 'gitlab'
diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml
index 224afbff39a..14d37b77a41 100644
--- a/app/views/admin/hooks/edit.html.haml
+++ b/app/views/admin/hooks/edit.html.haml
@@ -11,7 +11,7 @@
= gitlab_ui_form_for @hook, as: :hook, url: admin_hook_path do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
- %span>= f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mr-3'
+ %span>= f.submit _('Save changes'), class: 'gl-mr-3', pajamas_button: true
= render 'shared/web_hooks/test_button', hook: @hook
= link_to _('Delete'), admin_hook_path(@hook), method: :delete, class: 'btn gl-button btn-danger float-right', aria: { label: s_('Webhooks|Delete webhook') }, data: { confirm: s_('Webhooks|Are you sure you want to delete this webhook?'), confirm_btn_variant: 'danger' }
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index f23d77c8da5..d4aeb8dc7e8 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -7,7 +7,7 @@
.col-lg-8.gl-mb-3
= gitlab_ui_form_for @hook, as: :hook, url: admin_hooks_path do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
- = f.submit _('Add system hook'), class: 'btn gl-button btn-confirm'
+ = f.submit _('Add system hook'), pajamas_button: true
= render 'shared/web_hooks/index', hooks: @hooks, hook_class: @hook.class
diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml
index 2c526bb38d8..8cf1d8555ce 100644
--- a/app/views/admin/impersonation_tokens/index.html.haml
+++ b/app/views/admin/impersonation_tokens/index.html.haml
@@ -8,12 +8,10 @@
.row.gl-mt-3
.col-lg-12
- - if @new_impersonation_token
- = render 'shared/access_tokens/created_container',
- type: type,
- new_token_value: @new_impersonation_token
+ #js-new-access-token-app{ data: { access_token_type: type } }
= render 'shared/access_tokens/form',
+ ajax: true,
type: type,
title: _('Add an impersonation token'),
path: admin_user_impersonation_tokens_path,
@@ -22,9 +20,4 @@
scopes: @scopes,
help_path: help_page_path('api/index', anchor: 'impersonation-tokens')
- = render 'shared/access_tokens/table',
- type: type,
- type_plural: type_plural,
- impersonation: true,
- active_tokens: @active_impersonation_tokens,
- revoke_route_helper: ->(token) { revoke_admin_user_impersonation_token_path(token.user, token) }
+ #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_impersonation_tokens.to_json, information: _("To see all the user's personal access tokens you must impersonate them first.") } }
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index f56b77813b5..c7c30673d74 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -1,32 +1,33 @@
.js-projects-list-holder
- if @projects.any?
- %ul.projects-list.content-list.admin-projects
+ %ul.content-list
- @projects.each do |project|
- %li.project-row{ class: ('no-description' if project.description.blank?) }
- .controls
- = link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button btn-default"
- %button.delete-project-button.gl-button.btn.btn-danger{ data: { delete_project_url: admin_project_path(project), project_name: project.name } }
- = s_('AdminProjects|Delete')
+ %li.project-row.gl-align-items-center{ class: 'gl-display-flex!' }
+ .avatar-container.rect-avatar.s40.gl-flex-shrink-0
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
+ .gl-min-w-0.gl-flex-grow-1
+ .title
+ = link_to(admin_project_path(project)) do
+ %span.project-full-name
+ %span.namespace-name
+ - if project.namespace
+ = project.namespace.human_name
+ \/
+ %span.project-name
+ = project.name
- .stats
+ - if project.description.present?
+ .description
+ = markdown_field(project, :description)
+ .stats.gl-text-gray-500.gl-flex-shrink-0.gl-display-none.gl-sm-display-flex
= gl_badge_tag storage_counter(project.statistics&.storage_size)
= render_if_exists 'admin/projects/archived', project: project
- .title
- = link_to(admin_project_path(project)) do
- .dash-project-avatar
- .avatar-container.rect-avatar.s40
- = project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
- %span.project-full-name
- %span.namespace-name
- - if project.namespace
- = project.namespace.human_name
- \/
- %span.project-name
- = project.name
- - if project.description.present?
- .description
- = markdown_field(project, :description)
+ .controls.gl-flex-shrink-0.gl-ml-5
+ = render Pajamas::ButtonComponent.new(href: edit_project_path(project), button_options: { id: dom_id(project, :edit) }) do
+ = s_('Edit')
+ = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { class: 'delete-project-button', data: { delete_project_url: admin_project_path(project), project_name: project.name } } ) do
+ = s_('AdminProjects|Delete')
= paginate @projects, theme: 'gitlab'
- else
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index eabb7e51227..a60c3996cf2 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -2,7 +2,6 @@
- add_to_breadcrumbs _("Projects"), admin_projects_path
- breadcrumb_title @project.full_name
- page_title @project.full_name, _("Projects")
-- @content_class = "admin-projects"
- current_user_is_group_owner = @group && @group.has_owner?(current_user)
%h1.page-title.gl-font-size-h-display
diff --git a/app/views/admin/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml
index a9f5c560b41..3ccf3ef4f2a 100644
--- a/app/views/admin/users/_projects.html.haml
+++ b/app/views/admin/users/_projects.html.haml
@@ -1,13 +1,17 @@
- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present?
- .card.contributed-projects
- .card-header= _('Projects contributed to')
- = render 'shared/projects/list',
- projects: contributed_projects.sort_by(&:star_count).reverse,
- projects_limit: 5, stars: true, avatar: false
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-0' }) do |c|
+ - c.header do
+ = _('Projects contributed to')
+ - c.body do
+ = render 'shared/projects/list',
+ projects: contributed_projects.sort_by(&:star_count).reverse,
+ projects_limit: 5, stars: true, avatar: false
- if local_assigns.has_key?(:projects) && projects.present?
- .card
- .card-header= _('Personal projects')
- = render 'shared/projects/list',
- projects: projects.sort_by(&:star_count).reverse,
- projects_limit: 10, stars: true, avatar: false
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-0' }) do |c|
+ - c.header do
+ = _('Personal projects')
+ - c.body do
+ = render 'shared/projects/list',
+ projects: projects.sort_by(&:star_count).reverse,
+ projects_limit: 10, stars: true, avatar: false
diff --git a/app/views/admin/users/_user_detail_note.html.haml b/app/views/admin/users/_user_detail_note.html.haml
index cc4827327c9..c8625833a70 100644
--- a/app/views/admin/users/_user_detail_note.html.haml
+++ b/app/views/admin/users/_user_detail_note.html.haml
@@ -1,7 +1,7 @@
- if @user.note.present?
- text = @user.note
- .card
- .card-header
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-pb-0'}) do |c|
+ - c.header do
= _('Admin Note')
- .card-body
+ - c.body do
%p= text
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 2f6c08f123e..ff87cf8f866 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -4,20 +4,22 @@
= render 'admin/users/head'
- if @user.groups.any?
- .card
- .card-header= _('Groups')
- %ul.hover-list
- - @user.group_members.includes(:source).each do |group_member| # rubocop: disable CodeReuse/ActiveRecord
- - group = group_member.group
- %li.group_member
- %strong= link_to group.name, admin_group_path(group)
- &ndash; access to
- #{pluralize(group.projects.count, 'project')}
- .float-right
- %span.light.vertical-align-middle= group_member.human_access
- - unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member), confirm_btn_variant: 'danger', testid: 'remove-user' }, aria: { label: _('Remove') }, method: :delete, remote: true, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from group') do
- = sprite_icon('remove', size: 16, css_class: 'gl-icon')
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-0 gl-px-0'}) do |c|
+ - c.header do
+ = _('Groups')
+ - c.body do
+ %ul.hover-list
+ - @user.group_members.includes(:source).each do |group_member| # rubocop: disable CodeReuse/ActiveRecord
+ - group = group_member.group
+ %li.group_member
+ %strong= link_to group.name, admin_group_path(group)
+ &ndash; access to
+ #{pluralize(group.projects.count, 'project')}
+ .float-right
+ %span.light.vertical-align-middle= group_member.human_access
+ - unless group_member.owner?
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member), confirm_btn_variant: 'danger', testid: 'remove-user' }, aria: { label: _('Remove') }, method: :delete, remote: true, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from group') do
+ = sprite_icon('remove', size: 16, css_class: 'gl-icon')
.row
.col-md-6
@@ -28,23 +30,25 @@
.col-md-6
- .card
- .card-header= _('Joined projects (%{projects_count})') % { projects_count: @joined_projects.count }
- %ul.hover-list
- - @joined_projects.sort_by(&:full_name).each do |project|
- - member = project.team.find_member(@user.id)
- %li.project_member
- .list-item-name
- = link_to admin_project_path(project), class: dom_class(project) do
- = project.full_name
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-0 gl-px-0'}) do |c|
+ - c.header do
+ = _('Joined projects (%{projects_count})') % { projects_count: @joined_projects.count }
+ - c.body do
+ %ul.hover-list
+ - @joined_projects.sort_by(&:full_name).each do |project|
+ - member = project.team.find_member(@user.id)
+ %li.project_member
+ .list-item-name
+ = link_to admin_project_path(project), class: dom_class(project) do
+ = project.full_name
- - if member
- .float-right
- - if member.owner?
- %span.light= _('Owner')
- - else
- %span.light.vertical-align-middle= member.human_access
+ - if member
+ .float-right
+ - if member.owner?
+ %span.light= _('Owner')
+ - else
+ %span.light.vertical-align-middle= member.human_access
- - if member.respond_to? :project
- = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member), confirm_btn_variant: 'danger' }, aria: { label: _('Remove') }, remote: true, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from project') do
- = sprite_icon('remove', size: 16, css_class: 'gl-icon')
+ - if member.respond_to? :project
+ = link_to project_project_member_path(project, member), data: { confirm: remove_member_message(member), confirm_btn_variant: 'danger' }, aria: { label: _('Remove') }, remote: true, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon gl-ml-3", title: _('Remove user from project') do
+ = sprite_icon('remove', size: 16, css_class: 'gl-icon')
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 9197d6684e0..7edea81a123 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -5,138 +5,140 @@
.row
.col-md-6
- .card
- .card-header
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-2'}) do |c|
+ - c.header do
= @user.name
- %ul.content-list
- %li
- = image_tag avatar_icon_for_user(@user, 60, current_user: current_user), class: "avatar s60"
- %li
- %span.light= _('Profile page:')
- %strong
- = link_to user_path(@user) do
- = @user.username
+ - c.body do
+ %ul.content-list
+ %li
+ = render Pajamas::AvatarComponent.new(@user, size: 64, class: 'gl-mr-3')
+ %li
+ %span.light= _('Profile page:')
+ %strong
+ = link_to user_path(@user) do
+ = @user.username
-# Rendered on mobile only so order of cards can be different on desktop vs mobile
.gl-md-display-none
= render 'admin/users/profile', user: @user
= render 'admin/users/user_detail_note'
- .card
- .card-header
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-2'}) do |c|
+ - c.header do
= _('Account:')
- %ul.content-list
- %li
- %span.light= _('Name:')
- %strong= @user.name
- %li
- %span.light= _('Username:')
- %strong
- = @user.username
- %li
- %span.light= _('Email:')
- %strong
- = render partial: 'shared/email_with_badge', locals: { email: mail_to(@user.email), verified: @user.confirmed? }
- - @user.emails.reject(&:user_primary_email?).each do |email|
- %li
- %span.light= _('Secondary email:')
+ - c.body do
+ %ul.content-list
+ %li
+ %span.light= _('Name:')
+ %strong= @user.name
+ %li
+ %span.light= _('Username:')
%strong
- = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
- = link_to remove_email_admin_user_path(@user, email), data: { confirm: _("Are you sure you want to remove %{email}?") % { email: email.email }, 'confirm-btn-variant': 'danger' }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: _('Remove secondary email'), id: "remove_email_#{email.id}" do
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
- %li
- %span.light ID:
- %strong{ data: { qa_selector: 'user_id_content' } }
- = @user.id
- %li
- %span.light= _('Namespace ID:')
- %strong
- = @user.namespace_id
-
- %li.two-factor-status
- %span.light= _('Two-factor Authentication:')
- %strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' }
- - if @user.two_factor_enabled?
- = _('Enabled')
- = link_to _('Disable'), disable_two_factor_admin_user_path(@user), aria: { label: _('Disable') }, data: { confirm: _('Are you sure?'), 'confirm-btn-variant': 'danger' }, method: :patch, class: 'btn gl-button btn-sm btn-danger float-right', title: _('Disable Two-factor Authentication')
- - else
- = _('Disabled')
-
- = render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace
-
- %li
- %span.light= _('External User:')
- %strong
- = @user.external? ? _('Yes') : _('No')
-
- = render_if_exists 'admin/users/provisioned_by', user: @user
-
- %li
- %span.light= _('Can create groups:')
- %strong
- = @user.can_create_group ? _('Yes') : _('No')
- %li
- %span.light= _('Personal projects limit:')
- %strong
- = @user.projects_limit
- %li
- %span.light= _('Member since:')
- %strong
- = @user.created_at.to_s(:medium)
- - if @user.confirmed_at
- %li
- %span.light= _('Confirmed at:')
+ = @user.username
+ %li
+ %span.light= _('Email:')
%strong
- = @user.confirmed_at.to_s(:medium)
- - else
+ = render partial: 'shared/email_with_badge', locals: { email: mail_to(@user.email), verified: @user.confirmed? }
+ - @user.emails.reject(&:user_primary_email?).each do |email|
+ %li
+ %span.light= _('Secondary email:')
+ %strong
+ = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? }
+ = link_to remove_email_admin_user_path(@user, email), data: { confirm: _("Are you sure you want to remove %{email}?") % { email: email.email }, 'confirm-btn-variant': 'danger' }, method: :delete, class: "btn btn-sm btn-danger gl-button btn-icon float-right", title: _('Remove secondary email'), id: "remove_email_#{email.id}" do
+ = sprite_icon('close', size: 16, css_class: 'gl-icon')
%li
- %span.ligh= _('Confirmed:')
- %strong.cred
- = _('No')
+ %span.light ID:
+ %strong{ data: { qa_selector: 'user_id_content' } }
+ = @user.id
+ %li
+ %span.light= _('Namespace ID:')
+ %strong
+ = @user.namespace_id
- %li
- %span.light= _('Current sign-in IP:')
- %strong
- = @user.current_sign_in_ip || _('never')
+ %li.two-factor-status
+ %span.light= _('Two-factor Authentication:')
+ %strong{ class: @user.two_factor_enabled? ? 'cgreen' : 'cred' }
+ - if @user.two_factor_enabled?
+ = _('Enabled')
+ = link_to _('Disable'), disable_two_factor_admin_user_path(@user), aria: { label: _('Disable') }, data: { confirm: _('Are you sure?'), 'confirm-btn-variant': 'danger' }, method: :patch, class: 'btn gl-button btn-sm btn-danger float-right', title: _('Disable Two-factor Authentication')
+ - else
+ = _('Disabled')
- %li
- %span.light= _('Current sign-in at:')
- %strong
- = @user.current_sign_in_at&.to_s(:medium) || _('never')
+ = render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace
- %li
- %span.light= _('Last sign-in IP:')
- %strong
- = @user.last_sign_in_ip || _('never')
+ %li
+ %span.light= _('External User:')
+ %strong
+ = @user.external? ? _('Yes') : _('No')
- %li
- %span.light= _('Last sign-in at:')
- %strong
- = @user.last_sign_in_at&.to_s(:medium) || _('never')
+ = render_if_exists 'admin/users/provisioned_by', user: @user
- %li
- %span.light= _('Sign-in count:')
- %strong
- = @user.sign_in_count
+ %li
+ %span.light= _('Can create groups:')
+ %strong
+ = @user.can_create_group ? _('Yes') : _('No')
+ %li
+ %span.light= _('Personal projects limit:')
+ %strong
+ = @user.projects_limit
+ %li
+ %span.light= _('Member since:')
+ %strong
+ = @user.created_at.to_s(:medium)
+ - if @user.confirmed_at
+ %li
+ %span.light= _('Confirmed at:')
+ %strong
+ = @user.confirmed_at.to_s(:medium)
+ - else
+ %li
+ %span.ligh= _('Confirmed:')
+ %strong.cred
+ = _('No')
- %li
- %span.light= _("Highest role:")
- %strong
- = Gitlab::Access.human_access_with_none(@user.highest_role)
+ %li
+ %span.light= _('Current sign-in IP:')
+ %strong
+ = @user.current_sign_in_ip || _('never')
- = render_if_exists 'admin/users/using_license_seat', user: @user
+ %li
+ %span.light= _('Current sign-in at:')
+ %strong
+ = @user.current_sign_in_at&.to_s(:medium) || _('never')
- - if @user.ldap_user?
%li
- %span.light= _('LDAP uid:')
+ %span.light= _('Last sign-in IP:')
%strong
- = @user.ldap_identity.extern_uid
+ = @user.last_sign_in_ip || _('never')
- - if @user.created_by
%li
- %span.light= _('Created by:')
+ %span.light= _('Last sign-in at:')
%strong
- = link_to @user.created_by.name, [:admin, @user.created_by]
+ = @user.last_sign_in_at&.to_s(:medium) || _('never')
+
+ %li
+ %span.light= _('Sign-in count:')
+ %strong
+ = @user.sign_in_count
+
+ %li
+ %span.light= _("Highest role:")
+ %strong
+ = Gitlab::Access.human_access_with_none(@user.highest_role)
+
+ = render_if_exists 'admin/users/using_license_seat', user: @user
+
+ - if @user.ldap_user?
+ %li
+ %span.light= _('LDAP uid:')
+ %strong
+ = @user.ldap_identity.extern_uid
+
+ - if @user.created_by
+ %li
+ %span.light= _('Created by:')
+ %strong
+ = link_to @user.created_by.name, [:admin, @user.created_by]
- = render_if_exists 'namespaces/shared_runner_status', namespace: @user.namespace
+ = render_if_exists 'namespaces/shared_runner_status', namespace: @user.namespace
= render_if_exists 'admin/users/credit_card_info', user: @user, link_to_match_page: true
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 6ed46847482..3952a450c4a 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,7 +1,7 @@
- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
- if api_awards_path
- .gl-display-flex.gl-flex-wrap.gl-justify-content-space-between
+ .gl-display-flex.gl-flex-wrap.gl-justify-content-space-between.gl-py-3
#js-vue-awards-block{ data: { path: api_awards_path, can_award_emoji: can?(current_user, :award_emoji, awardable).to_s } }
= yield
- else
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 02c468cebd7..9ca11b35064 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -1,9 +1,11 @@
- save_endpoint = local_assigns.fetch(:save_endpoint, nil)
- if ci_variable_protected_by_default?
- %p.settings-message.text-center
- - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/index', anchor: 'protected-cicd-variables') }
- = s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ = render Pajamas::AlertComponent.new(variant: :warning, show_icon: false, dismissible: false,
+ alert_options: { class: 'gl-mb-3'}) do |c|
+ = c.body do
+ - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/index', anchor: 'protected-cicd-variables') }
+ = _('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- is_group = !@group.nil?
- is_project = !@project.nil?
diff --git a/app/views/ci/variables/_url_query_variable_row.html.haml b/app/views/ci/variables/_url_query_variable_row.html.haml
index 9c34daf88bd..77bcacdb94b 100644
--- a/app/views/ci/variables/_url_query_variable_row.html.haml
+++ b/app/views/ci/variables/_url_query_variable_row.html.haml
@@ -15,12 +15,12 @@
%input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
%select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name }
= options_for_select(ci_variable_type_options, variable_type)
- %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control.table-section.section-15{ type: "text",
+ %input.js-ci-variable-input-key.ci-variable-body-item.form-control.table-section.section-15{ type: "text",
name: key_input_name,
value: key,
placeholder: s_('CiVariables|Input variable key') }
.ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0
- %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control{ rows: 1,
+ %textarea.js-ci-variable-input-value.js-secret-value.form-control{ rows: 1,
name: value_input_name,
placeholder: s_('CiVariables|Input variable value') }
= value
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 483c767d029..c5e518d8526 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -18,14 +18,14 @@
%input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name }
%select.js-ci-variable-input-variable-type.ci-variable-body-item.form-control.select-control.custom-select.table-section.section-15{ name: variable_type_input_name }
= options_for_select(ci_variable_type_options, variable_type)
- %input.js-ci-variable-input-key.ci-variable-body-item.qa-ci-variable-input-key.form-control.gl-form-input.table-section.section-15{ type: "text",
+ %input.js-ci-variable-input-key.ci-variable-body-item.form-control.gl-form-input.table-section.section-15{ type: "text",
name: key_input_name,
value: key,
placeholder: s_('CiVariables|Input variable key') }
.ci-variable-body-item.gl-show-field-errors.table-section.section-15.border-top-0.p-0
- .form-control.js-secret-value-placeholder.qa-ci-variable-input-value.overflow-hidden{ class: ('hide' unless id) }
+ .form-control.js-secret-value-placeholder.overflow-hidden{ class: ('hide' unless id) }
= '*' * 17
- %textarea.js-ci-variable-input-value.js-secret-value.qa-ci-variable-input-value.form-control.gl-form-input{ class: ('hide' if id),
+ %textarea.js-ci-variable-input-value.js-secret-value.form-control.gl-form-input{ class: ('hide' if id),
rows: 1,
name: value_input_name,
placeholder: s_('CiVariables|Input variable value') }
diff --git a/app/views/clusters/clusters/_health.html.haml b/app/views/clusters/clusters/_health.html.haml
index 75609465eb3..9e7820d3136 100644
--- a/app/views/clusters/clusters/_health.html.haml
+++ b/app/views/clusters/clusters/_health.html.haml
@@ -1,4 +1,6 @@
-%section.settings.no-animate.expanded.cluster-health-graphs.qa-cluster-health-section#cluster-health
+- add_page_specific_style 'page_bundles/prometheus'
+
+%section.settings.no-animate.expanded.cluster-health-graphs#cluster-health
- if @cluster&.integration_prometheus_available?
#prometheus-graphs{ data: @cluster.health_data(clusterable) }
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index abe9cc9f27d..7bc782df119 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -1,3 +1,4 @@
+- add_page_specific_style 'page_bundles/clusters'
- breadcrumb_title _('Kubernetes')
- page_title _('Kubernetes Clusters')
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index bf7b24181c1..557c95f8478 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -36,7 +36,7 @@
= platform_kubernetes_field.form_group :authorization_type,
{ help: '%{help_text} %{help_link}'.html_safe % { help_text: rbac_help_text, help_link: rbac_help_link } } do
= platform_kubernetes_field.check_box :authorization_type,
- { class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'),
+ { data: { qa_selector: 'rbac_checkbox'}, label: s_('ClusterIntegration|RBAC-enabled cluster'),
label_class: 'label-bold', inline: true }, 'rbac', 'abac'
.form-group
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
index b6719834358..8a960602536 100644
--- a/app/views/devise/registrations/new.html.haml
+++ b/app/views/devise/registrations/new.html.haml
@@ -9,7 +9,7 @@
.signup-page
= render 'devise/shared/signup_box',
- url: registration_path(resource_name),
+ url: registration_path(resource_name, glm_tracking_params.to_hash),
button_text: _('Register'),
borderless: Feature.enabled?(:restyle_login_page, @project),
show_omniauth_providers: omniauth_enabled? && button_based_providers_enabled?
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index d06043c1750..7affbafbdeb 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -10,10 +10,10 @@
= label_tag :password
= password_field_tag :password, nil, { autocomplete: 'current-password', class: "form-control gl-form-input bottom", title: _("This field is required."), data: { qa_selector: 'password_field' }, required: true }
- if !hide_remember_me && devise_mapping.rememberable?
- .remember-me.gl-px-5
- %label{ for: "remember_me" }
- = check_box_tag :remember_me, '1', false, id: 'remember_me'
- %span= _('Remember me')
+ .gl-px-5
+ = render Pajamas::CheckboxTagComponent.new(name: 'remember_me') do |c|
+ = c.label do
+ = _('Remember me')
.submit-container.move-submit-down.gl-px-5.gl-pb-5
= submit_tag submit_message, class: "gl-button btn btn-confirm", data: { qa_selector: 'sign_in_button' }
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index d4f34a1cb3f..439a2fc4d96 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -13,8 +13,6 @@
%span.gl-button-text
= label_for_provider(provider)
- unless hide_remember_me
- %fieldset
- %label{ class: restyle_login_page_enabled ? 'gl-font-weight-normal' : '' }
- = check_box_tag :remember_me, nil, false
- %span
- = _('Remember me')
+ = render Pajamas::CheckboxTagComponent.new(name: 'remember_me', value: nil) do |c|
+ = c.label do
+ = _('Remember me')
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index e81a5928983..76c4cf41a2d 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -1,7 +1,7 @@
- show_password_form = local_assigns.fetch(:show_password_form, password_authentication_enabled_for_web?)
- render_signup_link = local_assigns.fetch(:render_signup_link, true)
-%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: "#{"custom-provider-tabs" if any_form_based_providers_enabled?} #{"nav-links-unboxed" if Feature.enabled?(:restyle_login_page, @project)}" }
+%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: "#{'custom-provider-tabs' if any_form_based_providers_enabled?} #{'nav-links-unboxed' if Feature.enabled?(:restyle_login_page, @project)}" }
- if crowd_enabled?
%li.nav-item
= link_to _("Crowd"), "#crowd", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:crowd))}", 'data-toggle' => 'tab', role: 'tab'
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 477f6c73388..224930e28df 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -3,19 +3,13 @@
.timeline-entry-inner
.timeline-content
.discussion.js-toggle-container{ data: { discussion_id: discussion.id, is_expanded: expanded.to_s } }
- .discussion-header
- .timeline-icon
+ .discussion-header.gl-display-flex.gl--flex-center
+ .timeline-icon.gl-flex-shrink-0
= link_to user_path(discussion.author) do
- = image_tag avatar_icon_for_user(discussion.author), class: "avatar s40"
- .discussion-actions
- %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button", class: ("js-toggle-lazy-diff" unless expanded) }
- = sprite_icon('chevron-up', css_class: "js-sidebar-collapse #{'hidden' unless expanded}")
- = sprite_icon('chevron-down', css_class: "js-sidebar-expand #{'hidden' if expanded}")
- %span.js-sidebar-collapse{ class: "#{'hidden' unless expanded}" }= _('Hide thread')
- %span.js-sidebar-expand{ class: "#{'hidden' if expanded}" }= _('Show thread')
+ = render Pajamas::AvatarComponent.new(discussion.author, size: 32, class: 'gl-mr-3')
= link_to_member(@project, discussion.author, avatar: false)
- .inline.discussion-headline-light
+ .inline.discussion-headline-light.gl-mx-3
= discussion.author.to_reference
started a thread
@@ -42,10 +36,16 @@
an old version of
the diff
-
= time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago")
= render "discussions/headline", discussion: discussion
+ .discussion-actions.gl-ml-auto
+ %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button", class: ("js-toggle-lazy-diff" unless expanded) }
+ = sprite_icon('chevron-up', css_class: "js-sidebar-collapse #{'hidden' unless expanded}")
+ = sprite_icon('chevron-down', css_class: "js-sidebar-expand #{'hidden' if expanded}")
+ %span.js-sidebar-collapse{ class: "#{'hidden' unless expanded}" }= _('Hide thread')
+ %span.js-sidebar-expand{ class: "#{'hidden' if expanded}" }= _('Show thread')
+
.discussion-body.js-toggle-content{ class: ("hide" unless expanded) }
- if discussion.diff_discussion? && discussion.diff_file
= render "discussions/diff_with_notes", discussion: discussion
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 1f6ac29bffc..7ef3461a7fb 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -6,7 +6,7 @@
= inline_event_icon(event)
- if event.target
%span.event-type.d-inline-block.gl-mr-2{ class: event.action_name }
- = event.action_name
+ = localized_action_name(event)
%span.event-target-type.gl-mr-2= event.target_type_name
= link_to event_target_path(event), class: 'has-tooltip event-target-link gl-mr-2', title: event.target_title do
= event.target.reference_link_text
diff --git a/app/views/groups/_import_group_from_another_instance_panel.html.haml b/app/views/groups/_import_group_from_another_instance_panel.html.haml
index a9234753aa2..d48bf0173a4 100644
--- a/app/views/groups/_import_group_from_another_instance_panel.html.haml
+++ b/app/views/groups/_import_group_from_another_instance_panel.html.haml
@@ -1,5 +1,5 @@
-= form_with url: configure_import_bulk_imports_path(namespace_id: params[:parent_id]), class: 'group-form gl-show-field-errors' do |f|
- .gl-border-l-solid.gl-border-r-solid.gl-border-gray-100.gl-border-1.gl-p-5
+= gitlab_ui_form_with url: configure_import_bulk_imports_path(namespace_id: params[:parent_id]), class: 'gl-show-field-errors' do |f|
+ .gl-border-l-solid.gl-border-r-solid.gl-border-t-solid.gl-border-gray-100.gl-border-1.gl-p-5.gl-mt-4
.gl-display-flex.gl-align-items-center
%h4.gl-display-flex
= s_('GroupsNew|Import groups from another instance of GitLab')
@@ -32,4 +32,4 @@
id: 'import_gitlab_token',
data: { qa_selector: 'import_gitlab_token' }
.gl-border-gray-100.gl-border-solid.gl-border-1.gl-bg-gray-10.gl-p-5
- = f.submit s_('GroupsNew|Connect instance'), class: 'btn gl-button btn-confirm', data: { qa_selector: 'connect_instance_button' }
+ = f.submit s_('GroupsNew|Connect instance'), pajamas_button: true, data: { qa_selector: 'connect_instance_button' }
diff --git a/app/views/groups/_import_group_from_file_panel.html.haml b/app/views/groups/_import_group_from_file_panel.html.haml
index 022777eea27..35e8b7dc977 100644
--- a/app/views/groups/_import_group_from_file_panel.html.haml
+++ b/app/views/groups/_import_group_from_file_panel.html.haml
@@ -2,7 +2,7 @@
- group_path = root_url
- group_path << parent.full_path + '/' if parent
-= form_for '', url: import_gitlab_group_path, namespace: 'import_group', class: 'group-form gl-show-field-errors', multipart: true do |f|
+= gitlab_ui_form_for '', url: import_gitlab_group_path, namespace: 'import_group', class: 'group-form gl-show-field-errors', multipart: true do |f|
.gl-border-l-solid.gl-border-r-solid.gl-border-gray-100.gl-border-1.gl-p-5
%h4
= _('Import group from file')
@@ -22,4 +22,4 @@
.gl-mt-3
= render 'shared/file_picker_button', f: f, field: :file, help_text: nil, classes: 'gl-button btn-confirm-secondary gl-mr-2'
.gl-border-gray-100.gl-border-solid.gl-border-1.gl-bg-gray-10.gl-p-5
- = f.submit _('Import'), class: 'btn gl-button btn-confirm'
+ = f.submit _('Import'), pajamas_button: true
diff --git a/app/views/groups/_personalize.html.haml b/app/views/groups/_personalize.html.haml
index bae76952ef8..2f55aefb817 100644
--- a/app/views/groups/_personalize.html.haml
+++ b/app/views/groups/_personalize.html.haml
@@ -8,7 +8,7 @@
.row
.form-group.col-sm-4
= label :user, :role, _('Role')
- = select :user, :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { selected: @current_user.role }, class: 'form-control'
+ = select :user, :role, ::User.roles.keys.map { |role| [localized_user_roles[role] || role.titleize, role] }, { selected: @current_user.role }, class: 'form-control'
.row
.form-group.col-sm-4
diff --git a/app/views/groups/boards/index.html.haml b/app/views/groups/boards/index.html.haml
index bb56769bd3f..e5b5f6404bb 100644
--- a/app/views/groups/boards/index.html.haml
+++ b/app/views/groups/boards/index.html.haml
@@ -1 +1 @@
-= render "shared/boards/show", board: @boards.first
+= render "shared/boards/show", board: @board
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 33fcda6129c..6c4a8b53764 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,24 +1,18 @@
-- @can_bulk_update = can?(current_user, :admin_merge_request, @group) && @group.licensed_feature_available?(:group_bulk_edit)
+- @can_bulk_update = can?(current_user, :admin_merge_request, @group) && @group.licensed_feature_available?(:group_bulk_edit) && issuables_count_for_state(:merge_requests, :all) > 0
- page_title _("Merge requests")
-- if issuables_count_for_state(:merge_requests, :all) == 0
- = render 'shared/issuable/search_bar', type: :merge_requests
+.top-area
+ = render 'shared/issuable/nav', type: :merge_requests
+ - if current_user
+ .nav-controls
+ - if @can_bulk_update
+ = render_if_exists 'projects/merge_requests/bulk_update_button'
- = render 'shared/empty_states/merge_requests', project_select_button: true
-- else
- .top-area
- = render 'shared/issuable/nav', type: :merge_requests
- - if current_user
- .nav-controls
- - if @can_bulk_update
- = render_if_exists 'projects/merge_requests/bulk_update_button'
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _("merge request"), type: :merge_requests, with_feature_enabled: 'merge_requests', with_shared: false, include_projects_in_subgroups: true
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _("merge request"), type: :merge_requests, with_feature_enabled: 'merge_requests', with_shared: false, include_projects_in_subgroups: true
+= render 'shared/issuable/search_bar', type: :merge_requests
+- if @can_bulk_update
+ = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests
- = render 'shared/issuable/search_bar', type: :merge_requests
-
- - if @can_bulk_update
- = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :merge_requests
-
- = render 'shared/merge_requests'
+= render 'shared/merge_requests'
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index 3864b30eb1e..d4b1c3c27f1 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -21,8 +21,8 @@
.form-actions
- if @milestone.new_record?
- = f.submit _('Create milestone'), class: "btn-confirm gl-button btn", data: { qa_selector: "create_milestone_button" }
+ = f.submit _('Create milestone'), data: { qa_selector: "create_milestone_button" }, pajamas_button: true
= link_to _("Cancel"), group_milestones_path(@group), class: "btn gl-button btn-cancel"
- else
- = f.submit _('Update milestone'), class: "btn-confirm gl-button btn"
+ = f.submit _('Update milestone'), pajamas_button: true
= link_to _("Cancel"), group_milestone_path(@group, @milestone), class: "btn gl-button btn-cancel"
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 8384c906eeb..657a582bdc5 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -16,7 +16,7 @@
#import-group-pane.tab-pane
- if import_sources_enabled?
- - if Feature.enabled?(:bulk_import)
+ - if BulkImports::Features.enabled?
= render 'import_group_from_another_instance_panel'
.gl-mt-7.gl-border-b-solid.gl-border-gray-100.gl-border-1
= render 'import_group_from_file_panel'
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index d5c22d9b1f2..e7ae54a8879 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -2,45 +2,46 @@
- page_title _("Projects")
- @content_class = "limit-container-width" unless fluid_layout
-.card.gl-mt-3
- .card-header
- %strong= @group.name
- projects:
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-mt-3 js-search-settings-section' }, header_options: { class: 'gl-display-flex' }, body_options: { class: 'gl-py-0' }) do |c|
+ - c.header do
+ .gl-flex-grow-1
+ = html_escape(_("%{strong_open}%{group_name}%{strong_close} projects:")) % { strong_open: '<strong>'.html_safe, group_name: @group.name, strong_close: '</strong>'.html_safe }
- if can? current_user, :admin_group, @group
.controls
- = link_to new_project_path(namespace_id: @group.id), class: "btn gl-button btn-sm btn-confirm" do
- New project
- %ul.projects-list.content-list.group-settings-projects
- - @projects.each do |project|
- %li.project-row{ class: ('no-description' if project.description.blank?) }
- .controls
- = link_to _('Members'), project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button"
- = link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn gl-button"
- = render 'delete_project_button', project: project
+ = render Pajamas::ButtonComponent.new(href: new_project_path(namespace_id: @group.id), size: :small, variant: :confirm) do
+ = _("New project")
+ - c.body do
+ %ul.content-list
+ - @projects.each do |project|
+ %li.project-row.gl-align-items-center{ class: 'gl-display-flex!' }
+ .avatar-container.rect-avatar.s40.gl-flex-shrink-0
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
+ .gl-min-w-0.gl-flex-grow-1
+ .title
+ = link_to project_path(project), class: 'js-prefetch-document' do
+ %span.project-full-name
+ %span.namespace-name
+ - if project.namespace
+ = project.namespace.human_name
+ \/
+ %span.project-name
+ = project.name
+ %span{ class: visibility_level_color(project.visibility_level) }
+ = visibility_level_icon(project.visibility_level)
- .stats
- = gl_badge_tag storage_counter(project.statistics&.storage_size)
- = render 'project_badges', project: project
+ - if project.description.present?
+ .description
+ = markdown_field(project, :description)
- .title
- = link_to project_path(project), class: 'js-prefetch-document' do
- .dash-project-avatar
- .avatar-container.rect-avatar.s40
- = project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
- %span.project-full-name
- %span.namespace-name
- - if project.namespace
- = project.namespace.human_name
- \/
- %span.project-name
- = project.name
- %span{ class: visibility_level_color(project.visibility_level) }
- = visibility_level_icon(project.visibility_level)
+ .stats.gl-text-gray-500.gl-flex-shrink-0.gl-display-none.gl-sm-display-flex
+ = gl_badge_tag storage_counter(project.statistics&.storage_size)
+ = render 'project_badges', project: project
- - if project.description.present?
- .description
- = markdown_field(project, :description)
- - if @projects.blank?
- .nothing-here-block This group has no projects yet
+ .controls.gl-flex-shrink-0.gl-ml-5
+ = link_to _('Members'), project_project_members_path(project), id: dom_id(project, :edit), class: "btn gl-button"
+ = link_to _('Edit'), edit_project_path(project), id: dom_id(project, :edit), class: "btn gl-button"
+ = render 'delete_project_button', project: project
+ - if @projects.blank?
+ .nothing-here-block= _("This group has no projects yet")
= paginate @projects, theme: "gitlab"
diff --git a/app/views/groups/settings/_pages_settings.html.haml b/app/views/groups/settings/_pages_settings.html.haml
index 456e0b0f1d0..441e9333630 100644
--- a/app/views/groups/settings/_pages_settings.html.haml
+++ b/app/views/groups/settings/_pages_settings.html.haml
@@ -1,5 +1,5 @@
-= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
+= gitlab_ui_form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
= render_if_exists 'shared/pages/max_pages_size_input', form: f
.gl-mt-3
- = f.submit s_('GitLabPages|Save changes'), class: 'btn gl-button btn-confirm'
+ = f.submit s_('GitLabPages|Save changes'), pajamas_button: true
diff --git a/app/views/groups/settings/access_tokens/index.html.haml b/app/views/groups/settings/access_tokens/index.html.haml
index ac6c5d1842c..5e3d814687e 100644
--- a/app/views/groups/settings/access_tokens/index.html.haml
+++ b/app/views/groups/settings/access_tokens/index.html.haml
@@ -4,7 +4,7 @@
- type_plural = _('group access tokens')
- @content_class = 'limit-container-width' unless fluid_layout
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4
%h4.gl-mt-0
= page_title
@@ -24,13 +24,11 @@
= _('You can enable group access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.col-lg-8
- - if @new_resource_access_token
- = render 'shared/access_tokens/created_container',
- type: type,
- new_token_value: @new_resource_access_token
+ #js-new-access-token-app{ data: { access_token_type: type } }
- if current_user.can?(:create_resource_access_tokens, @group)
= render 'shared/access_tokens/form',
+ ajax: true,
type: type,
path: group_settings_access_tokens_path(@group),
resource: @group,
@@ -41,10 +39,6 @@
prefix: :resource_access_token,
help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token')
- = render 'shared/access_tokens/table',
- active_tokens: @active_resource_access_tokens,
- resource: @group,
- type: type,
- type_plural: type_plural,
- revoke_route_helper: ->(token) { revoke_group_settings_access_token_path(id: token) },
- no_active_tokens_message: _('This group has no active access tokens.')
+ #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_resource_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true
+ } }
+
diff --git a/app/views/groups/settings/ci_cd/_form.html.haml b/app/views/groups/settings/ci_cd/_form.html.haml
index 59c67197f81..89e353b94b0 100644
--- a/app/views/groups/settings/ci_cd/_form.html.haml
+++ b/app/views/groups/settings/ci_cd/_form.html.haml
@@ -1,6 +1,6 @@
.row.gl-mt-3
.col-lg-12
- = form_for group, url: group_settings_ci_cd_path(group, anchor: 'js-general-pipeline-settings') do |f|
+ = gitlab_ui_form_for group, url: group_settings_ci_cd_path(group, anchor: 'js-general-pipeline-settings') do |f|
%fieldset.builds-feature
.form-group
= f.label :max_artifacts_size, _('Maximum artifacts size'), class: 'label-bold'
@@ -8,4 +8,4 @@
%p.form-text.text-muted
= _("The maximum file size in megabytes for individual job artifacts.")
= link_to s_('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size'), target: '_blank', rel: 'noopener noreferrer'
- = f.submit _('Save changes'), class: "btn gl-button btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 88352ea351c..67b87f842f9 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -44,7 +44,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
- auto_devops_url = help_page_path('topics/autodevops/index')
- - quickstart_url = help_page_path('topics/autodevops/quick_start_guide')
+ - quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke')
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
- quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url }
= s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
diff --git a/app/views/groups/settings/integrations/index.html.haml b/app/views/groups/settings/integrations/index.html.haml
index 1db8edb040b..ec99ceb5f8d 100644
--- a/app/views/groups/settings/integrations/index.html.haml
+++ b/app/views/groups/settings/integrations/index.html.haml
@@ -2,8 +2,9 @@
- page_title s_('Integrations|Group-level integration management')
- @content_class = 'limit-container-width' unless fluid_layout
-%h3= s_('Integrations|Group-level integration management')
+%section.js-search-settings-section
+ %h3= s_('Integrations|Group-level integration management')
-- integrations_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: integrations_help_page_path }
-%p= s_("Integrations|GitLab administrators can set up integrations that all projects in a group inherit and use by default. These integrations apply to all projects that don't already use custom settings. You can override custom settings for a project if the settings are necessary at that level. Learn more about %{integrations_link_start}group-level integration management%{link_end}.").html_safe % { integrations_link_start: integrations_link_start, link_end: "</a>".html_safe }
-= render 'shared/integrations/index', integrations: @integrations
+ - integrations_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: integrations_help_page_path }
+ %p= s_("Integrations|GitLab administrators can set up integrations that all projects in a group inherit and use by default. These integrations apply to all projects that don't already use custom settings. You can override custom settings for a project if the settings are necessary at that level. Learn more about %{integrations_link_start}group-level integration management%{link_end}.").html_safe % { integrations_link_start: integrations_link_start, link_end: "</a>".html_safe }
+ = render 'shared/integrations/index', integrations: @integrations
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index f474f8fbd3b..012a31c1ecf 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -3,9 +3,6 @@
- @skip_current_level_breadcrumb = true
- add_page_specific_style 'page_bundles/group'
-- if show_thanks_for_purchase_alert?
- = render_if_exists 'shared/thanks_for_purchase_alert', plan_title: plan_title, quantity: params[:purchased_quantity].to_i
-
= render_if_exists 'shared/qrtly_reconciliation_alert', group: @group
= render_if_exists 'shared/free_user_cap_alert', source: @group
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 3992cb527ed..eaa58580454 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -34,14 +34,14 @@
.row.gl-mt-3
.col-md-8
- .documentation-index.md
+ .md
= markdown(@help_index)
.col-md-4
.card.links-card
.card-header
= _('Quick help')
%ul.content-list
- %li= link_to _('See our website for help'), support_url
+ %li= link_to _('See our website for help'), support_url, { class: 'gl-text-blue-600!' }
%li
%button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
= _('Use the search bar on the top of this page')
@@ -49,5 +49,5 @@
%button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' }
= _('Use shortcuts')
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
- %li= link_to _('Get a support subscription'), "https://#{ApplicationHelper.promo_host}/pricing/"
- %li= link_to _('Compare GitLab editions'), "https://#{ApplicationHelper.promo_host}/features/#compare"
+ %li= link_to _('Get a support subscription'), "https://#{ApplicationHelper.promo_host}/pricing/", { class: 'gl-text-blue-600!' }
+ %li= link_to _('Compare GitLab editions'), "https://#{ApplicationHelper.promo_host}/features/#compare", { class: 'gl-text-blue-600!' }
diff --git a/app/views/help/instance_configuration.html.haml b/app/views/help/instance_configuration.html.haml
index b6d27123be4..4baedca820f 100644
--- a/app/views/help/instance_configuration.html.haml
+++ b/app/views/help/instance_configuration.html.haml
@@ -1,5 +1,5 @@
- page_title _('Instance Configuration')
-.documentation.md
+.md.gl-font-lg.gl-mt-3
%h1= _('Instance Configuration')
%p
diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml
index c41f6ea3ed4..39f45e8b649 100644
--- a/app/views/help/show.html.haml
+++ b/app/views/help/show.html.haml
@@ -1,5 +1,5 @@
- page_title @path.split("/").reverse.map(&:humanize)
- @content_class = "limit-container-width" unless fluid_layout
-.documentation.md.gl-mt-3
+.md.gl-font-lg.gl-mt-3
= markdown @markdown
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index 35fd5d6eda6..9ea52a8f82f 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -5,6 +5,7 @@
- paginatable = local_assigns.fetch(:paginatable, false)
- default_namespace_path = (local_assigns[:default_namespace] || current_user.namespace).full_path
- provider_title = Gitlab::ImportSources.title(provider)
+- optional_stages = local_assigns.fetch(:optional_stages, [])
- header_title _("New project"), new_project_path
- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
@@ -18,4 +19,5 @@
default_target_namespace: default_namespace_path,
import_path: url_for([:import, provider, { format: :json }]),
filterable: filterable.to_s,
- paginatable: paginatable.to_s }.merge(extra_data) }
+ paginatable: paginatable.to_s,
+ optional_stages: optional_stages.to_json }.merge(extra_data) }
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index 28836055e0e..9d4c0f62134 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -23,23 +23,21 @@
%p
= html_escape(_('Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. "By %{link_open}@johnsmith%{link_close}"). It will also associate and/or assign these issues and comments with the selected user.')) % { link_open: '<a href="#">'.html_safe, link_close: '</a>'.html_safe }
- .table-holder
- %table.table
- %thead
+ %table.table
+ %thead
+ %tr
+ %th= _("ID")
+ %th= _("Name")
+ %th= _("Email")
+ %th= _("GitLab User")
+ %tbody
+ - @user_map.each do |id, user|
%tr
- %th= _("ID")
- %th= _("Name")
- %th= _("Email")
- %th= _("GitLab User")
- %tbody
- - @user_map.each do |id, user|
- %tr
- %td= id
- %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
- %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
- %td
- = users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control',
- scope: :all, email_user: true, selected: user[:gitlab_user])
+ %td= id
+ %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control gl-form-input'
+ %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control gl-form-input'
+ %td
+ .js-gitlab-user{ data: { name: "users[#{id}][gitlab_user]" } }
.form-actions
= submit_tag _('Continue to the next step'), class: 'gl-button btn btn-confirm'
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 1b556cd0f7f..25afe9a7b1b 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -7,4 +7,7 @@
- paginatable = Feature.enabled?(:remove_legacy_github_client)
-= render 'import/githubish_status', provider: 'github', paginatable: paginatable, default_namespace: @namespace
+= render 'import/githubish_status',
+ provider: 'github', paginatable: paginatable,
+ default_namespace: @namespace,
+ optional_stages: Gitlab::GithubImport::Settings.stages_array
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index ab4b3cf6afd..5a558d42802 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,8 +1,10 @@
--# We currently only support `alert`, `notice`, `success`, 'toast', and 'raw'
-- icons = {'alert' => 'error', 'notice' => 'information-o', 'success' => 'check-circle'}
-- type_to_variant = {'alert' => 'danger', 'notice' => 'info', 'success' => 'success'}
+- flash_container_no_margin = local_assigns.fetch(:flash_container_no_margin, false)
+- flash_container_class = ('flash-container-no-margin' if flash_container_no_margin)
+
+-# We currently only support `alert`, `notice`, `success`, `warning`, 'toast', and 'raw'
+- type_to_variant = {'alert' => 'danger', 'notice' => 'info', 'success' => 'success', 'warning' => 'warning'}
- closable = %w[alert notice success]
-.flash-container.flash-container-page.sticky{ data: { qa_selector: 'flash_container' } }
+.flash-container.flash-container-page.sticky{ data: { qa_selector: 'flash_container' }, class: flash_container_class }
- flash.each do |key, value|
- if key == 'toast' && value
.js-toast-message{ data: { message: value } }
@@ -11,9 +13,5 @@
- elsif value == I18n.t('devise.failure.unconfirmed')
= render 'shared/confirm_your_email_alert'
- elsif value
- %div{ class: "flash-#{key} mb-2", data: { testid: "alert-#{type_to_variant[key]}" } }
- = sprite_icon(icons[key], css_class: 'align-middle mr-1') unless icons[key].nil?
- %span= value
- - if closable.include?(key)
- %div{ class: "close-icon-wrapper js-close-icon" }
- = sprite_icon('close', css_class: 'close-icon gl-vertical-align-baseline!')
+ = render Pajamas::AlertComponent.new(variant: type_to_variant[key], dismissible: closable.include?(key), alert_options: {class: "flash-#{key}", data: { testid: "alert-#{type_to_variant[key]}" }}) do |c|
+ = c.with_body { value }
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 84eb2706929..2ac926a7fc3 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,5 +1,5 @@
- page_description brand_title unless page_description
-- site_name = "GitLab"
+- site_name = _('GitLab')
%head{ prefix: "og: http://ogp.me/ns#" }
%meta{ charset: "utf-8" }
diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml
index 61a57240ed5..f4f9f39c20e 100644
--- a/app/views/layouts/fullscreen.html.haml
+++ b/app/views/layouts/fullscreen.html.haml
@@ -1,11 +1,13 @@
+- minimal = local_assigns.fetch(:minimal, false)
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
%body{ class: "#{user_application_theme} #{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
= render 'peek/bar'
= header_message
- = render partial: "layouts/header/default", locals: { project: @project, group: @group }
- .mobile-overlay
+ - unless minimal
+ = render partial: "layouts/header/default", locals: { project: @project, group: @group }
+ .mobile-overlay
.hide-when-top-nav-responsive-open.gl--flex-full.gl-h-full{ class: nav ? ["layout-page", page_with_sidebar_class, "gl-mt-0!"]: '' }
- if defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}"
@@ -14,8 +16,9 @@
= render 'shared/outdated_browser'
= render "layouts/broadcast"
= yield :flash_message
- = render "layouts/flash"
+ = render "layouts/flash", flash_container_no_margin: true
.content-wrapper{ id: "content-body", class: "d-flex flex-column align-items-stretch" }
= yield
- = render "layouts/nav/top_nav_responsive", class: "gl-flex-grow-1 gl-overflow-y-auto"
+ - unless minimal
+ = render "layouts/nav/top_nav_responsive", class: "gl-flex-grow-1 gl-overflow-y-auto"
= footer_message
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index a00c5c186cc..b74dfd4d3a1 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -132,7 +132,7 @@
- if header_link?(:user_dropdown)
%li.nav-item.header-user.js-nav-user-dropdown.dropdown{ data: { track_label: "profile_dropdown", track_action: "click_dropdown", track_value: "", qa_selector: 'user_menu', testid: 'user-menu' }, class: ('mr-0' if has_impersonation_link) }
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
- = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar", alt: current_user.name
+ = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'header-user-avatar', avatar_options: { data: { qa_selector: 'user_avatar_content' } })
= render_if_exists 'layouts/header/user_notification_dot', project: project, namespace: group
= sprite_icon('chevron-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
diff --git a/app/views/layouts/header/_gitlab_version.html.haml b/app/views/layouts/header/_gitlab_version.html.haml
index 125fbaa084c..fae6926a687 100644
--- a/app/views/layouts/header/_gitlab_version.html.haml
+++ b/app/views/layouts/header/_gitlab_version.html.haml
@@ -1,6 +1,6 @@
- return unless show_version_check?
-.gl-display-flex.gl-flex-direction-column.gl-px-4.gl-py-3
+%a{ class: 'gl-display-flex! gl-flex-direction-column gl-px-4! gl-py-3! gl-line-height-24!', href: help_page_path('update/index'), 'data-testid': 'gitlab-version-container' }
%span
= s_("VersionCheck|Your GitLab Version")
= emoji_icon('rocket')
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index 3a8f9c1ae8d..bdd1ae291fd 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -1,6 +1,7 @@
%ul
- if current_user_menu?(:help)
- = render 'layouts/header/gitlab_version'
+ %li
+ = render 'layouts/header/gitlab_version'
= render_if_exists 'layouts/header/help_dropdown/cross_stage_fdm'
= render 'layouts/header/whats_new_dropdown_item'
%li
diff --git a/app/views/layouts/header/_new_dropdown.html.haml b/app/views/layouts/header/_new_dropdown.html.haml
index e5b03acbe3b..9801b0cc055 100644
--- a/app/views/layouts/header/_new_dropdown.html.haml
+++ b/app/views/layouts/header/_new_dropdown.html.haml
@@ -24,4 +24,4 @@
= menu_item.fetch(:title)
- if menu_item.fetch(:emoji)
-# We need to insert a space between the title and emoji
- = " #{emoji_icon(menu_item.fetch(:emoji), 'aria-hidden': true, class: "gl-font-base gl-vertical-align-baseline")}".html_safe
+ = " #{emoji_icon(menu_item.fetch(:emoji), 'aria-hidden': true, class: 'gl-font-base gl-vertical-align-baseline')}".html_safe
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 56f333664df..8815dec5a6b 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -1,4 +1,4 @@
-%aside.nav-sidebar.qa-admin-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation') }
+%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation'), data: { qa_selector: 'admin_sidebar_content' } }
.nav-sidebar-inner-scroll
.context-header
= link_to admin_root_path, title: _('Admin Overview'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
@@ -6,7 +6,7 @@
= sprite_icon('admin', size: 18)
%span.sidebar-context-title
= _('Admin Area')
- %ul.sidebar-top-level-items{ data: { qa_selector: 'admin_sidebar_overview_submenu_content' } }
+ %ul.sidebar-top-level-items{ data: { qa_selector: 'admin_overview_submenu_content' } }
= nav_link(controller: %w[dashboard admin admin/projects users groups admin/topics jobs runners gitaly_servers cohorts], html_options: {class: 'home'}) do
= link_to admin_root_path, class: 'has-sub-items' do
.nav-icon-container
@@ -28,15 +28,15 @@
%span
= _('Projects')
= nav_link(controller: %w[users cohorts]) do
- = link_to admin_users_path, title: _('Users'), data: { qa_selector: 'users_overview_link' } do
+ = link_to admin_users_path, title: _('Users'), data: { qa_selector: 'admin_overview_users_link' } do
%span
= _('Users')
= nav_link(controller: :groups) do
- = link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'groups_overview_link' } do
+ = link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'admin_overview_groups_link' } do
%span
= _('Groups')
= nav_link(controller: [:admin, 'admin/topics']) do
- = link_to admin_topics_path, title: _('Topics'), data: { qa_selector: 'topics_overview_link' } do
+ = link_to admin_topics_path, title: _('Topics') do
%span
= _('Topics')
= nav_link path: 'jobs#index' do
@@ -75,13 +75,13 @@
= _('Usage Trends')
= nav_link(controller: admin_monitoring_nav_links) do
- = link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_link' }, class: 'has-sub-items' do
+ = link_to admin_system_info_path, data: { qa_selector: 'admin_monitoring_menu_link' }, class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('monitor')
%span.nav-item-name
= _('Monitoring')
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_monitoring_submenu_content' } }
+ %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_monitoring_submenu_content' } }
= nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_system_info_path do
%strong.fly-out-top-item-name
@@ -222,10 +222,10 @@
= link_to general_admin_application_settings_path, class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('settings')
- %span.nav-item-name.qa-admin-settings-item
+ %span.nav-item-name{ data: { qa_selector: 'admin_settings_menu_link' } }
= _('Settings')
- %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_settings_submenu_content' } }
+ %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_settings_submenu_content' } }
-# This active_nav_link check is also used in `app/views/layouts/admin.html.haml`
= nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" } ) do
= link_to general_admin_application_settings_path do
@@ -233,24 +233,24 @@
= _('Settings')
%li.divider.fly-out-top-item
= nav_link(path: 'application_settings#general') do
- = link_to general_admin_application_settings_path, title: _('General'), class: 'qa-admin-settings-general-item' do
+ = link_to general_admin_application_settings_path, title: _('General'), data: { qa_selector: 'admin_settings_general_link' } do
%span
= _('General')
- = render_if_exists 'layouts/nav/sidebar/advanced_search', class: 'qa-admin-settings-advanced-search'
+ = render_if_exists 'layouts/nav/sidebar/advanced_search', data: { qa_selector: 'admin_settings_advanced_search_link' }
- if instance_level_integrations?
= nav_link(path: ['application_settings#integrations', 'integrations#edit']) do
- = link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'integration_settings_link' } do
+ = link_to integrations_admin_application_settings_path, title: _('Integrations'), data: { qa_selector: 'admin_settings_integrations_link' } do
%span
= _('Integrations')
= nav_link(path: 'application_settings#repository') do
- = link_to repository_admin_application_settings_path, title: _('Repository'), class: 'qa-admin-settings-repository-item' do
+ = link_to repository_admin_application_settings_path, title: _('Repository'), data: { qa_selector: 'admin_settings_repository_link' } do
%span
= _('Repository')
- if Gitlab.ee? && License.feature_available?(:custom_file_templates)
= nav_link(path: 'application_settings#templates') do
- = link_to templates_admin_application_settings_path, title: _('Templates'), class: 'qa-admin-settings-template-item' do
+ = link_to templates_admin_application_settings_path, title: _('Templates'), data: { qa_selector: 'admin_settings_templates_link' } do
%span
= _('Templates')
= nav_link(path: 'application_settings#ci_cd') do
@@ -262,7 +262,7 @@
%span
= _('Reporting')
= nav_link(path: 'application_settings#metrics_and_profiling') do
- = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling'), class: 'qa-admin-settings-metrics-and-profiling-item' do
+ = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling'), data: { qa_selector: 'admin_settings_metrics_and_profiling_link' } do
%span
= _('Metrics and profiling')
= nav_link(path: ['application_settings#service_usage_data']) do
@@ -270,7 +270,7 @@
%span
= _('Service usage data')
= nav_link(path: 'application_settings#network') do
- = link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_item' } do
+ = link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_link' } do
%span
= _('Network')
= nav_link(controller: :appearances ) do
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index cf1f84790a2..0e3327935ca 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -2,8 +2,7 @@
.nav-sidebar-inner-scroll
.context-header
= link_to profile_path, title: _('Profile Settings'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
- %span{ class: ['avatar-container', 'settings-avatar', 's32'] }
- = image_tag avatar_icon_for_user(current_user, 32), class: ['avatar', 'avatar-tile', 'js-sidebar-user-avatar', 's32', 'gl-rounded-full!'], alt: current_user.name, data: { testid: 'sidebar-user-avatar' }
+ = render Pajamas::AvatarComponent.new(current_user, size: 32, alt: current_user.name, class: 'gl-mr-3 js-sidebar-user-avatar', avatar_options: { data: { testid: 'sidebar-user-avatar' } })
%span.sidebar-context-title= _('User Settings')
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
@@ -52,17 +51,18 @@
= link_to profile_chat_names_path do
%strong.fly-out-top-item-name
= _('Chat')
- = nav_link(controller: :personal_access_tokens) do
- = link_to profile_personal_access_tokens_path do
- .nav-icon-container
- = sprite_icon('token')
- %span.nav-item-name
- = _('Access Tokens')
- %ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" } ) do
- = link_to profile_personal_access_tokens_path do
- %strong.fly-out-top-item-name
- = _('Access Tokens')
+ - unless Gitlab::CurrentSettings.personal_access_tokens_disabled?
+ = nav_link(controller: :personal_access_tokens) do
+ = link_to profile_personal_access_tokens_path do
+ .nav-icon-container
+ = sprite_icon('token')
+ %span.nav-item-name
+ = _('Access Tokens')
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_personal_access_tokens_path do
+ %strong.fly-out-top-item-name
+ = _('Access Tokens')
= nav_link(controller: :emails) do
= link_to profile_emails_path, data: { qa_selector: 'profile_emails_link' } do
.nav-icon-container
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index d05b6951fbf..c557dc36534 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -24,7 +24,7 @@
- if @reply_by_email
= _('Reply to this email directly or %{view_it_on_gitlab}.').html_safe % { view_it_on_gitlab: link_to(_("view it on GitLab"), @target_url) }
- else
- #{link_to _("View it on GitLab"), @target_url}.
+ #{link_to _('View it on GitLab'), @target_url}.
%br
= notification_reason_text(reason: @reason, show_manage_notifications_link: !@labels_url, show_help_link: true, manage_label_subscriptions_url: @labels_url, unsubscribe_url: @unsubscribe_url, format: :html)
diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml
index c9baf0cd2b8..032be73f70c 100644
--- a/app/views/layouts/terms.html.haml
+++ b/app/views/layouts/terms.html.haml
@@ -25,7 +25,7 @@
%ul.nav.navbar-nav
%li.header-user.dropdown
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
- = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar", data: { qa_selector: 'user_avatar' }
+ = render Pajamas::AvatarComponent.new(current_user, size: 24, class: 'gl-mr-3', avatar_options: { data: { qa_selector: 'user_avatar_content' } })
= sprite_icon('chevron-down')
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown'
diff --git a/app/views/notify/access_token_revoked_email.html.haml b/app/views/notify/access_token_revoked_email.html.haml
new file mode 100644
index 00000000000..4d9b9e14d14
--- /dev/null
+++ b/app/views/notify/access_token_revoked_email.html.haml
@@ -0,0 +1,7 @@
+%p
+ = _('Hi %{username}!') % { username: sanitize_name(@user.name) }
+%p
+ = html_escape(_('A personal access token, named %{code_start}%{token_name}%{code_end}, has been revoked.')) % { code_start: '<code>'.html_safe, token_name: @token_name, code_end: '</code>'.html_safe }
+%p
+ - pat_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
+ = html_escape(_('You can check your tokens or create a new one in your %{pat_link_start}personal access tokens settings%{pat_link_end}.')) % { pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe }
diff --git a/app/views/notify/access_token_revoked_email.text.erb b/app/views/notify/access_token_revoked_email.text.erb
new file mode 100644
index 00000000000..17dd628d76c
--- /dev/null
+++ b/app/views/notify/access_token_revoked_email.text.erb
@@ -0,0 +1,5 @@
+<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
+
+<%= _('A personal access token, named %{token_name}, has been revoked.') % { token_name: @token_name } %>
+
+<%= _('You can check your tokens or create a new one in your personal access tokens settings %{pat_link}.') % { pat_link: @target_url } %>
diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml
index 71c62f6be4e..11d761414ff 100644
--- a/app/views/notify/project_was_exported_email.html.haml
+++ b/app/views/notify/project_was_exported_email.html.haml
@@ -1,8 +1,8 @@
%p
- Project #{@project.name} was exported successfully.
+ = s_('Notify|Project %{project_name} was exported successfully.') % {project_name: @project.name}
%p
- The project export can be downloaded from:
- = link_to download_export_project_url(@project), rel: 'nofollow', download: '' do
- = @project.full_name + " export"
+ - project_link_url = download_export_project_url(@project)
+ - project_link_start = '<a href="%{url}" target="_blank" rel="nofollow" download="">'.html_safe % { url: project_link_url }
+ = html_escape(s_('Notify|%{project_link_start}Download%{project_link_end} the project export.')) % {project_link_start: project_link_start, project_link_end: '</a>'.html_safe}
%p
- The download link will expire in 24 hours.
+ = s_('Notify|The download link will expire in 24 hours.')
diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml
index 1b6b1a81665..45bec221e08 100644
--- a/app/views/notify/project_was_moved_email.html.haml
+++ b/app/views/notify/project_was_moved_email.html.haml
@@ -1,15 +1,9 @@
+- repo_url_styles = "background: #f5f5f5; padding:10px; border:1px solid #ddd"
+- ssh_url_to_repo = content_tag(:p, "git remote set-url origin #{strip_tags(@project.ssh_url_to_repo)}", style: repo_url_styles)
+- http_url_to_repo = content_tag(:p, "git remote set-url origin #{strip_tags(@project.http_url_to_repo)}", style: repo_url_styles)
%p
- Project #{@old_path_with_namespace} was moved to another location
+ = s_('Notify|Project %{old_path_with_namespace} was moved to another location.') % { old_path_with_namespace: @old_path_with_namespace }
%p
- The project is now located under
- = link_to project_url(@project) do
- = @project.full_name
-%p
- To update the remote url in your local repository run (for ssh):
-%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" }
- git remote set-url origin #{@project.ssh_url_to_repo}
-%p
- or for http(s):
-%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" }
- git remote set-url origin #{@project.http_url_to_repo}
-%br
+ - project_full_name_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: project_url(@project) }
+ = html_escape(s_('Notify|The project is now located under %{project_full_name_link_start}%{project_full_name}%{link_end}.')) % { project_full_name_link_start: project_full_name_link_start, link_end: '</a>'.html_safe, project_full_name: @project.full_name }
+= html_escape(s_('Notify|%{p_start}To update the remote url in your local repository run (for ssh):%{p_end} %{ssh_url_to_repo} %{p_start}or for http(s):%{p_end} %{http_url_to_repo}')) % { p_start: '<p>'.html_safe, p_end: '</p>'.html_safe, ssh_url_to_repo: ssh_url_to_repo, http_url_to_repo: http_url_to_repo }
diff --git a/app/views/notify/project_was_not_exported_email.html.haml b/app/views/notify/project_was_not_exported_email.html.haml
index c888da29c17..dcd212099b5 100644
--- a/app/views/notify/project_was_not_exported_email.html.haml
+++ b/app/views/notify/project_was_not_exported_email.html.haml
@@ -1,7 +1,7 @@
%p
- Project #{@project.name} couldn't be exported.
+ = s_("Notify|Project %{project_name} couldn't be exported.") % {project_name: @project.name}
%p
- The errors we encountered were:
+ = s_('Notify|The errors we encountered were:')
%ul
- @errors.each do |error|
diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml
index 895d8807e47..2ba0a2cf4ab 100644
--- a/app/views/notify/repository_push_email.text.haml
+++ b/app/views/notify/repository_push_email.text.haml
@@ -13,7 +13,7 @@
\- - - - -
\
\
- #{pluralize @message.diffs_count, "changed file"}:
+ #{pluralize @message.diffs_count, 'changed file'}:
\
- @message.diffs.each do |diff_file|
- if diff_file.deleted_file?
diff --git a/app/views/notify/request_review_merge_request_email.html.haml b/app/views/notify/request_review_merge_request_email.html.haml
index d1f72f6529a..a8c7df79ff3 100644
--- a/app/views/notify/request_review_merge_request_email.html.haml
+++ b/app/views/notify/request_review_merge_request_email.html.haml
@@ -1,2 +1,2 @@
%p
- #{sanitize_name(@updated_by.name)} requested a new review on #{merge_request_reference_link(@merge_request)}.
+ = html_escape(s_('Notify|%{name} requested a new review on %{mr_link}.')) % {name: sanitize_name(@updated_by.name), mr_link: merge_request_reference_link(@merge_request).html_safe}
diff --git a/app/views/notify/send_unsubscribed_notification.html.haml b/app/views/notify/send_unsubscribed_notification.html.haml
index 9f68feeaa31..ef1577dde97 100644
--- a/app/views/notify/send_unsubscribed_notification.html.haml
+++ b/app/views/notify/send_unsubscribed_notification.html.haml
@@ -1,2 +1,2 @@
%p
- You have been unsubscribed from receiving GitLab administrator notifications.
+ = s_('Notify|You have been unsubscribed from receiving GitLab administrator notifications.')
diff --git a/app/views/notify/two_factor_otp_attempt_failed_email.html.haml b/app/views/notify/two_factor_otp_attempt_failed_email.html.haml
new file mode 100644
index 00000000000..fec7083e524
--- /dev/null
+++ b/app/views/notify/two_factor_otp_attempt_failed_email.html.haml
@@ -0,0 +1,51 @@
+- default_font = "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;"
+- default_style = "#{default_font}font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;"
+- spacer_style = "#{default_font};height:18px;font-size:18px;line-height:18px;"
+
+%tr.alert
+ %td{ style: "#{default_font}padding:10px;border-radius:3px;font-size:14px;line-height:1.3;text-align:center;overflow:hidden;color:#ffffff;background-color:#FC6D26;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "#{default_font}vertical-align:middle;color:#ffffff;text-align:center;" }
+ %span
+ = _("We detected an attempt to sign in to your %{host} account using a wrong two-factor authentication code") % { host: Gitlab.config.gitlab.host }
+%tr.spacer
+ %td{ style: spacer_style }
+ &nbsp;
+%tr.section
+ %td{ style: "#{default_font};padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;" }
+ %table.info{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;" }
+ %tbody
+ %tr
+ %td{ style: default_style }
+ = _('Hostname')
+ %td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;" }
+ = Gitlab.config.gitlab.host
+ %tr
+ %td{ style: "#{default_style}border-top:1px solid #ededed;" }
+ = _('IP Address')
+ %td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %span.muted{ style: "color:#333333;text-decoration:none;" }
+ = @ip
+ %tr
+ %td{ style: "#{default_style}border-top:1px solid #ededed;" }
+ = _('Time')
+ %td{ style: "#{default_style}color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ = @time.strftime('%Y-%m-%d %H:%M:%S %Z')
+%tr.spacer
+ %td{ style: spacer_style }
+ &nbsp;
+%tr.section
+ %td{ style: "#{default_font};line-height:1.4;text-align:center;padding:0 15px;overflow:hidden;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;width:100%;" }
+ %tbody
+ %tr{ style: 'width:100%;' }
+ %td{ style: "#{default_style}text-align:center;" }
+ - password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password' }
+ = _('If you recently tried to sign in, but mistakenly entered a wrong two-factor authentication code, you may ignore this email.')
+
+ - if password_authentication_enabled_for_web?
+ %p
+ = _('If you did not recently try to sign in, you should immediately %{password_link_start}change your password%{password_link_end}.').html_safe % { password_link_start: password_link_start, password_link_end: '</a>'.html_safe }
+ = _('Make sure you choose a strong, unique password.')
diff --git a/app/views/notify/two_factor_otp_attempt_failed_email.text.haml b/app/views/notify/two_factor_otp_attempt_failed_email.text.haml
new file mode 100644
index 00000000000..8f839cd83ee
--- /dev/null
+++ b/app/views/notify/two_factor_otp_attempt_failed_email.text.haml
@@ -0,0 +1,7 @@
+= _('Hi %{username}!') % { username: sanitize_name(@user.name) }
+
+= _('We detected an attempt to sign in to your %{host} account using a wrong two-factor authentication code, from the following IP address: %{ip}, at %{time}') % { host: Gitlab.config.gitlab.host, ip: @ip, time: @time }
+
+= _('If you recently tried to sign in, but mistakenly entered a wrong two-factor authentication code, you may ignore this email.')
+= _('If you did not recently try to sign in, you should immediately change your password: %{password_link}.') % { password_link: 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password' }
+= _('Make sure you choose a strong, unique password.')
diff --git a/app/views/notify/unknown_sign_in_email.html.haml b/app/views/notify/unknown_sign_in_email.html.haml
index 47c5656db27..b1c79274e26 100644
--- a/app/views/notify/unknown_sign_in_email.html.haml
+++ b/app/views/notify/unknown_sign_in_email.html.haml
@@ -42,7 +42,7 @@
%tbody
%tr{ style: 'width:100%;' }
%td{ style: "#{default_style}text-align:center;" }
- - password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' }
+ - password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password' }
= _('If you recently signed in and recognize the IP address, you may disregard this email.')
- if password_authentication_enabled_for_web?
diff --git a/app/views/notify/unknown_sign_in_email.text.haml b/app/views/notify/unknown_sign_in_email.text.haml
index f3efc4c4fcd..54c7a245ab9 100644
--- a/app/views/notify/unknown_sign_in_email.text.haml
+++ b/app/views/notify/unknown_sign_in_email.text.haml
@@ -3,7 +3,7 @@
= _('A sign-in to your account has been made from the following IP address: %{ip}') % { ip: @ip }
= _('If you recently signed in and recognize the IP address, you may disregard this email.')
-= _('If you did not recently sign in, you should immediately change your password: %{password_link}.') % { password_link: 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' }
+= _('If you did not recently sign in, you should immediately change your password: %{password_link}.') % { password_link: 'https://docs.gitlab.com/ee/user/profile/user_passwords.html#change-your-password' }
= _('Passwords should be unique and not used for any other sites or services.')
- unless @user.two_factor_enabled?
diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml
index be835233528..e9e6ca3ecce 100644
--- a/app/views/profiles/active_sessions/index.html.haml
+++ b/app/views/profiles/active_sessions/index.html.haml
@@ -1,7 +1,7 @@
- page_title _('Active Sessions')
- @content_class = "limit-container-width" unless fluid_layout
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= page_title
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index 4bbb4a21b39..9997c8c4b4c 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -1,7 +1,7 @@
- page_title _('Authentication log')
- @content_class = "limit-container-width" unless fluid_layout
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= page_title
diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml
index 54c34228800..41bd81d0250 100644
--- a/app/views/profiles/chat_names/index.html.haml
+++ b/app/views/profiles/chat_names/index.html.haml
@@ -1,7 +1,7 @@
- page_title _('Chat')
- @content_class = "limit-container-width" unless fluid_layout
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= page_title
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 1b8f0328a04..f4513d15a30 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,7 +1,7 @@
- page_title _('Emails')
- @content_class = "limit-container-width" unless fluid_layout
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= page_title
@@ -10,12 +10,12 @@
.col-lg-8
%h4.gl-mt-0
= _('Add email address')
- = form_for 'email', url: profile_emails_path do |f|
+ = gitlab_ui_form_for 'email', url: profile_emails_path do |f|
.form-group
= f.label :email, _('Email'), class: 'label-bold'
= f.text_field :email, class: 'form-control gl-form-input', data: { qa_selector: 'email_address_field' }
.gl-mt-3
- = f.submit _('Add email address'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'add_email_address_button' }
+ = f.submit _('Add email address'), data: { qa_selector: 'add_email_address_button' }, pajamas_button: true
%hr
%h4.gl-mt-0
= _('Linked emails (%{email_count})') % { email_count: @emails.load.size }
diff --git a/app/views/profiles/gpg_keys/_form.html.haml b/app/views/profiles/gpg_keys/_form.html.haml
index 9804a3b7735..ffd8bc3de27 100644
--- a/app/views/profiles/gpg_keys/_form.html.haml
+++ b/app/views/profiles/gpg_keys/_form.html.haml
@@ -1,5 +1,5 @@
%div
- = form_for [:profile, @gpg_key], html: { class: 'js-requires-input' } do |f|
+ = gitlab_ui_form_for [:profile, @gpg_key], html: { class: 'js-requires-input' } do |f|
= form_errors(@gpg_key)
.form-group
@@ -7,4 +7,4 @@
= f.text_area :key, class: "form-control gl-form-input", rows: 8, required: true, placeholder: _("Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'.")
.gl-mt-3
- = f.submit s_('Profiles|Add key'), class: "gl-button btn btn-confirm"
+ = f.submit s_('Profiles|Add key'), pajamas_button: true
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
index 91af6953ee1..539a0cd1f0e 100644
--- a/app/views/profiles/gpg_keys/index.html.haml
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -1,7 +1,8 @@
- page_title _('GPG Keys')
+- add_page_specific_style 'page_bundles/profile'
- @content_class = "limit-container-width" unless fluid_layout
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= page_title
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 6f7eb21b7e0..b37a0d9cc1a 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -1,6 +1,6 @@
- max_date = ::Gitlab::CurrentSettings.max_ssh_key_lifetime_from_now.to_date if ssh_key_expiration_policy_enabled?
%div
- = form_for [:profile, @key], html: { class: 'js-requires-input' } do |f|
+ = gitlab_ui_form_for [:profile, @key], html: { class: 'js-requires-input' } do |f|
= form_errors(@key)
.form-group
@@ -29,4 +29,4 @@
button_options: { class: 'js-add-ssh-key-validation-confirm-submit' }) do
= _("Yes, add it")
.gl-mt-3
- = f.submit s_('Profiles|Add key'), class: "gl-button btn btn-confirm js-add-ssh-key-validation-original-submit qa-add-key-button"
+ = f.submit s_('Profiles|Add key'), class: "js-add-ssh-key-validation-original-submit qa-add-key-button", pajamas_button: true
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 8016d989ff1..04fa1d96204 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -1,39 +1,40 @@
- is_admin = defined?(admin) ? true : false
.row.gl-mt-3
.col-md-4
- .card
- .card-header
+ = render Pajamas::CardComponent.new(body_options: { class: 'gl-py-0'}) do |c|
+ - c.header do
= _('SSH Key')
- %ul.content-list
- %li
- %span.light= _('Title:')
- %strong= @key.title
- %li
- %span.light= _('Created on:')
- %strong= @key.created_at.to_s(:medium)
- %li
- %span.light= _('Expires:')
- %strong= @key.expires_at.try(:to_s, :medium) || _('Never')
- %li
- %span.light= _('Last used on:')
- %strong= @key.last_used_at.try(:to_s, :medium) || _('Never')
+ - c.body do
+ %ul.content-list
+ %li
+ %span.light= _('Title:')
+ %strong= @key.title
+ %li
+ %span.light= _('Created on:')
+ %strong= @key.created_at.to_s(:medium)
+ %li
+ %span.light= _('Expires:')
+ %strong= @key.expires_at.try(:to_s, :medium) || _('Never')
+ %li
+ %span.light= _('Last used on:')
+ %strong= @key.last_used_at.try(:to_s, :medium) || _('Never')
.col-md-8
= form_errors(@key, type: 'key') unless @key.valid?
%pre.well-pre
= @key.key
- .card
- .card-header
+ = render Pajamas::CardComponent.new(body_options: { class: 'gl-py-0'}) do |c|
+ - c.header do
= _('Fingerprints')
- %ul.content-list
- %li
- %span.light= 'MD5:'
- %code.key-fingerprint= @key.fingerprint
- - if @key.fingerprint_sha256.present?
+ - c.body do
+ %ul.content-list
%li
- %span.light= 'SHA256:'
- %code.key-fingerprint= @key.fingerprint_sha256
-
+ %span.light= 'MD5:'
+ %code.key-fingerprint= @key.fingerprint
+ - if @key.fingerprint_sha256.present?
+ %li
+ %span.light= 'SHA256:'
+ %code.key-fingerprint= @key.fingerprint_sha256
.col-md-12
.float-right
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 35bf7d81502..69e92b9e508 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,7 +1,8 @@
- page_title _('SSH Keys')
+- add_page_specific_style 'page_bundles/profile'
- @content_class = "limit-container-width" unless fluid_layout
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= page_title
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 0f4b130a774..23a0d824bfe 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -10,7 +10,7 @@
%li= msg
= hidden_field_tag :notification_type, 'global'
- .row.gl-mt-3
+ .row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= page_title
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index 257255eb4d7..99c89dcebb4 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -2,7 +2,7 @@
- page_title _('Password')
- @content_class = "limit-container-width" unless fluid_layout
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= page_title
@@ -14,7 +14,7 @@
= _('Change your password')
- else
= _('Change your password or recover your current one')
- = form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f|
+ = gitlab_ui_form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f|
= form_errors(@user)
- unless @user.password_automatically_set?
@@ -31,6 +31,7 @@
= f.label :password_confirmation, _('Password confirmation'), class: 'label-bold'
= f.password_field :password_confirmation, required: true, autocomplete: 'new-password', class: 'form-control gl-form-input', data: { qa_selector: 'confirm_password_field' }
.gl-mt-3.gl-mb-3
- = f.submit _('Save password'), class: "gl-button btn btn-confirm gl-mr-3", data: { qa_selector: 'save_password_button' }
+ = f.submit _('Save password'), class: "gl-mr-3", data: { qa_selector: 'save_password_button' }, pajamas_button: true
- unless @user.password_automatically_set?
- = link_to _('I forgot my password'), reset_profile_password_path, method: :put
+ = render Pajamas::ButtonComponent.new(href: reset_profile_password_path, variant: :link, method: :put) do
+ = _('I forgot my password')
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index 6f260eb4cc0..a0a9077afe4 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -3,7 +3,7 @@
%h1.page-title.gl-font-size-h-display= _('Set up new password')
%hr
-= form_for @user, url: profile_password_path, method: :post do |f|
+= gitlab_ui_form_for @user, url: profile_password_path, method: :post do |f|
%p.slead
= _('Please set a new password before proceeding.')
%br
@@ -29,4 +29,4 @@
.col-sm-10
= f.password_field :password_confirmation, required: true, autocomplete: 'new-password', class: 'form-control gl-form-input', data: { qa_selector: 'confirm_password_field' }
.form-actions
- = f.submit _('Set new password'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'set_new_password_button' }
+ = f.submit _('Set new password'), data: { qa_selector: 'set_new_password_button' }, pajamas_button: true
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index e16108c5c22..a1d6ef3fec5 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -104,6 +104,10 @@
= f.gitlab_ui_checkbox_component :markdown_surround_selection,
s_('Preferences|Surround text selection when typing quotes or brackets'),
help_text: sprintf(s_( "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}."), { supported_characters: supported_characters }).html_safe
+ .form-group
+ = f.gitlab_ui_checkbox_component :markdown_automatic_lists,
+ s_('Preferences|Automatically add new list items'),
+ help_text: html_escape(s_('Preferences|When you type in a description or comment box, pressing %{kbdOpen}Enter%{kbdClose} in a list adds a new item below.')) % { kbdOpen: '<kbd>'.html_safe, kbdClose: '</kbd>'.html_safe }
.form-group
= f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index f38d6021b18..dfaa4c31cdf 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title s_("Profiles|Edit Profile")
- page_title s_("Profiles|Edit Profile")
+- add_page_specific_style 'page_bundles/profile'
- @content_class = "limit-container-width" unless fluid_layout
- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
@@ -25,15 +26,21 @@
.col-lg-8
.avatar-image
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
- = image_tag avatar_icon_for_user(@user, 96), alt: '', class: 'avatar s96'
+ = render Pajamas::AvatarComponent.new(@user, size: 96, alt: "", class: 'gl-float-left gl-mr-5')
%h5.gl-mt-0= s_("Profiles|Upload new avatar")
- .gl-my-3
- %button.gl-button.btn.btn-default.js-choose-user-avatar-button{ type: 'button' }= s_("Profiles|Choose file...")
- %span.avatar-file-name.gl-ml-3.js-avatar-filename= s_("Profiles|No file chosen.")
+ .gl-display-flex.gl-align-items-center.gl-my-3
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-choose-user-avatar-button' }) do
+ = s_("Profiles|Choose file...")
+ %span.gl-ml-3.js-avatar-filename= s_("Profiles|No file chosen.")
= f.file_field :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
.gl-text-gray-500= s_("Profiles|The maximum file size allowed is 200KB.")
- if @user.avatar?
- = link_to s_("Profiles|Remove avatar"), profile_avatar_path, data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") }, method: :delete, class: 'gl-button btn btn-danger-secondary btn-sm gl-mt-5'
+ = render Pajamas::ButtonComponent.new(variant: :danger,
+ category: :secondary,
+ href: profile_avatar_path,
+ button_options: { class: 'gl-mt-3', data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") } },
+ method: :delete) do
+ = s_("Profiles|Remove avatar")
.col-lg-12
%hr
.row.js-search-settings-section
@@ -54,9 +61,8 @@
%h4.gl-mt-0= s_("Profiles|Time settings")
%p= s_("Profiles|Set your local time zone.")
.col-lg-8
- %h5= _("Time zone")
- = dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg gl-w-full!', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
- %input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone }
+ = f.label :user_timezone, _("Time zone")
+ .js-timezone-dropdown{ data: { timezone_data: timezone_data.to_json, initial_value: @user.timezone } }
.col-lg-12
%hr
.row.js-search-settings-section
@@ -134,9 +140,12 @@
= f.gitlab_ui_checkbox_component :include_private_contributions,
s_('Profiles|Include private contributions on my profile'),
help_text: s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.")
- %hr
- = f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3 js-password-prompt-btn'
- = link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel'
+ .row.js-hide-when-nothing-matches-search
+ .col-lg-12
+ %hr
+ = f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true
+ = render Pajamas::ButtonComponent.new(href: user_path(current_user)) do
+ = s_('TagsPage|Cancel')
#password-prompt-modal
@@ -146,19 +155,19 @@
.modal-header
%h4.modal-title
= s_("Profiles|Position and size your new avatar")
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _("Close") }
- %span{ "aria-hidden": "true" } &times;
+ = render Pajamas::ButtonComponent.new(category: :tertiary,
+ icon: 'close',
+ button_options: { class: 'close', "data-dismiss": "modal", "aria-label" => _("Close") })
.modal-body
.profile-crop-image-container
%img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") }
- .crop-controls
+ .gl-text-center.gl-mt-4
.btn-group
- %button.btn.gl-button.btn-default{ data: { method: 'zoom', option: '-0.1' } }
- %span
- = sprite_icon('search-minus')
- %button.btn.gl-button.btn-default{ data: { method: 'zoom', option: '0.1' } }
- %span
- = sprite_icon('search-plus')
+ = render Pajamas::ButtonComponent.new(icon: 'search-minus',
+ button_options: {data: { method: 'zoom', option: '-0.1' }})
+ = render Pajamas::ButtonComponent.new(icon: 'search-plus',
+ button_options: {data: { method: 'zoom', option: '0.1' }})
.modal-footer
- %button.btn.gl-button.btn-confirm.js-upload-user-avatar{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(variant: :confirm,
+ button_options: { class: 'js-upload-user-avatar'}) do
= s_("Profiles|Set new profile picture")
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 855c73fd323..4c045574834 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -17,6 +17,13 @@
= _("You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication.")
%p
= _('If you lose your recovery codes you can generate new ones, invalidating all previous codes.')
+ - if @error
+ = render Pajamas::AlertComponent.new(title: @error[:message],
+ variant: :danger,
+ alert_options: { class: 'gl-mb-3' },
+ dismissible: false) do |c|
+ = c.body do
+ = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
.js-manage-two-factor-form{ data: { webauthn_enabled: webauthn_enabled, current_password_required: current_password_required?.to_s, profile_two_factor_auth_path: profile_two_factor_auth_path, profile_two_factor_auth_method: 'delete', codes_profile_two_factor_auth_path: codes_profile_two_factor_auth_path, codes_profile_two_factor_auth_method: 'post' } }
- else
@@ -46,6 +53,7 @@
- if @error
= render Pajamas::AlertComponent.new(title: @error[:message],
variant: :danger,
+ alert_options: { class: 'gl-mb-3' },
dismissible: false) do |c|
= c.body do
= link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/_fork_suggestion.html.haml b/app/views/projects/_fork_suggestion.html.haml
index 55e609c0ffb..47d60593b4a 100644
--- a/app/views/projects/_fork_suggestion.html.haml
+++ b/app/views/projects/_fork_suggestion.html.haml
@@ -2,6 +2,7 @@
- message = message_base.html_safe % { edit_start: '<span class="js-file-fork-suggestion-section-action">'.html_safe, edit_end: '</span>'.html_safe }
.js-file-fork-suggestion-section.file-fork-suggestion.hidden
%span.file-fork-suggestion-note= message
- = link_to s_('ForkSuggestion|Fork'), nil, method: :post, class: 'js-fork-suggestion-button gl-button btn btn-grouped btn-confirm-secondary'
- %button.js-cancel-fork-suggestion-button.gl-button.btn.btn-grouped{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, category: :secondary, button_options: { class: "js-fork-suggestion-button btn-grouped" }) do
+ = s_('ForkSuggestion|Fork')
+ = render Pajamas::ButtonComponent.new(button_options: { class: "js-cancel-fork-suggestion-button btn-grouped" }) do
= s_('ForkSuggestion|Cancel')
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 7ff58d12b9c..a862b841008 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -24,7 +24,7 @@
%span.gl-ml-3.gl-mb-3
= render 'shared/members/access_request_links', source: @project
- = cache_if(cache_enabled, [@project, :buttons, current_user, @notification_setting], expires_in: 1.day) do
+ = cache_if(cache_enabled, [@project, @project.star_count, @project.forks_count, :buttons, current_user, @notification_setting], expires_in: 1.day) do
.project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5
- if current_user
- if current_user.admin?
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 98cd831d6f1..0699e39b420 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -92,5 +92,5 @@
-# this partial is from JiHu, see details in https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/675
= render_if_exists 'shared/other_project_options', f: f, visibility_level: visibility_level, track_label: track_label
-= f.submit _('Create project'), class: "btn gl-button btn-confirm js-create-project-button", data: { qa_selector: 'project_create_button', track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" }
+= f.submit _('Create project'), class: "js-create-project-button", data: { qa_selector: 'project_create_button', track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" }, pajamas_button: true
= link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_action: "click_button", track_property: "cancel", track_value: "" }
diff --git a/app/views/projects/_transfer.html.haml b/app/views/projects/_transfer.html.haml
index 02aa1f7e93b..e3aa2d8afc9 100644
--- a/app/views/projects/_transfer.html.haml
+++ b/app/views/projects/_transfer.html.haml
@@ -1,7 +1,7 @@
- return unless can?(current_user, :change_namespace, @project)
- form_id = "transfer-project-form"
- hidden_input_id = "new_namespace_id"
-- initial_data = { button_text: s_('ProjectSettings|Transfer project'), confirm_danger_message: transfer_project_message(@project), phrase: @project.name, target_form_id: form_id, target_hidden_input_id: hidden_input_id }
+- initial_data = { button_text: s_('ProjectSettings|Transfer project'), confirm_danger_message: transfer_project_message(@project), phrase: @project.name, target_form_id: form_id, target_hidden_input_id: hidden_input_id, project_id: @project.id }
.sub-section
%h4.danger-title= _('Transfer project')
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 3ebac785d55..c91dfe6d28e 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _('Artifacts')
- page_title @path.presence, _('Artifacts'), "#{@build.name} (##{@build.id})", _('Jobs')
+- add_page_specific_style 'page_bundles/tree'
= render "projects/jobs/header"
diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml
index 1ad70506be4..e16e3ef266d 100644
--- a/app/views/projects/artifacts/file.html.haml
+++ b/app/views/projects/artifacts/file.html.haml
@@ -1,4 +1,5 @@
- page_title @path, _('Artifacts'), "#{@build.name} (##{@build.id})", _('Jobs')
+- add_page_specific_style 'page_bundles/tree'
= render "projects/jobs/header"
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index f2c4fe017f2..dd041377b49 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,6 +1,7 @@
- page_title _("Blame"), @blob.path, @ref
+- add_page_specific_style 'page_bundles/tree'
-#blob-content-holder.tree-holder{ data: { testid: 'blob-content-holder' } }
+#blob-content-holder.tree-holder.js-per-page{ data: { testid: 'blob-content-holder', per_page: @blame_per_page } }
= render "projects/blob/breadcrumb", blob: @blob, blame: true
.file-holder.gl-overflow-hidden
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 4139be053f8..9fd542e0cfb 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -10,7 +10,7 @@
%ul.blob-commit-info
= render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref
- = render_if_exists 'projects/blob/owners', blob: blob
+ #js-code-owners{ data: { blob_path: blob.path, project_path: @project.full_path, branch: @ref } }
= render "projects/blob/auxiliary_viewer", blob: blob
#blob-content-holder.blob-content-holder
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 398ca3dd27c..bd08ab67cd3 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -28,16 +28,12 @@
.file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end
- if is_markdown
- = render 'shared/blob/markdown_buttons', show_fullscreen_button: false
- = button_tag class: 'soft-wrap-toggle btn gl-button btn-default', type: 'button', tabindex: '-1' do
- .no-wrap
- = sprite_icon('soft-unwrap', css_class: 'gl-button-icon')
- %span.gl-button-text
- No wrap
- .soft-wrap
- = sprite_icon('soft-wrap', css_class: 'gl-button-icon')
- %span.gl-button-text
- Soft wrap
+ = render 'shared/blob/markdown_buttons', show_fullscreen_button: false, supports_file_upload: false
+ %span.soft-wrap-toggle
+ = render Pajamas::ButtonComponent.new(icon: 'soft-unwrap', button_options: { class: 'no-wrap' }) do
+ = _("No wrap")
+ = render Pajamas::ButtonComponent.new(icon: 'soft-wrap', button_options: { class: 'soft-wrap' }) do
+ = _("Soft wrap")
.file-editor.code
- if Feature.enabled?(:source_editor_toolbar, current_user)
diff --git a/app/views/projects/blob/_pipeline_tour_success.html.haml b/app/views/projects/blob/_pipeline_tour_success.html.haml
index 8f1c2f93162..0fa4a90e28b 100644
--- a/app/views/projects/blob/_pipeline_tour_success.html.haml
+++ b/app/views/projects/blob/_pipeline_tour_success.html.haml
@@ -2,5 +2,5 @@
'go-to-pipelines-path': project_pipelines_path(@project),
'project-merge-requests-path': project_merge_requests_path(@project),
'example-link': help_page_path('ci/examples/index.md', anchor: 'gitlab-cicd-examples'),
- 'code-quality-link': help_page_path('user/project/merge_requests/code_quality'),
+ 'code-quality-link': help_page_path('ci/testing/code_quality'),
'human-access': @project.team.human_max_access(current_user&.id) } }
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
index a76e61bc3dd..249c474587c 100644
--- a/app/views/projects/blob/_template_selectors.html.haml
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -2,14 +2,14 @@
.template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden
- toggle_text = should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : 'Select a template type'
- = dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector qa-template-type-dropdown', dropdown_class: 'dropdown-menu-selectable' })
+ = dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector', dropdown_class: 'dropdown-menu-selectable', data: { qa_selector: 'template_type_dropdown' } })
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector qa-license-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name, qa_selector: 'license_dropdown' } } )
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector qa-gitignore-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project) } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project), qa_selector: 'gitignore_dropdown' } } )
.metrics-dashboard-selector.js-metrics-dashboard-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector qa-metrics-dashboard-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project) } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project), qa_selector: 'metrics_dashboard_dropdown' } } )
#gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector qa-gitlab-ci-yml-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template] } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template], qa_selector: 'gitlab_ci_yml_dropdown' } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector qa-dockerfile-dropdown', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project) } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project), qa_selector: 'dockerfile_dropdown' } } )
diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml
index 41a0045be89..6f559708d40 100644
--- a/app/views/projects/blob/preview.html.haml
+++ b/app/views/projects/blob/preview.html.haml
@@ -1,4 +1,4 @@
-- if markup?(@blob.name)
+- if Gitlab::MarkupHelper.markup?(@blob.name)
.file-content.md
= markup(@blob.name, @content)
- else
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 33b2229f5d1..c8cf12c36f9 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _('Repository')
- page_title @blob.path, @ref
+- add_page_specific_style 'page_bundles/tree'
- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1)
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco', prefetch: true)
diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml
index bb56769bd3f..e5b5f6404bb 100644
--- a/app/views/projects/boards/index.html.haml
+++ b/app/views/projects/boards/index.html.haml
@@ -1 +1 @@
-= render "shared/boards/show", board: @boards.first
+= render "shared/boards/show", board: @board
diff --git a/app/views/projects/branch_rules/_show.html.haml b/app/views/projects/branch_rules/_show.html.haml
index 46665fdb450..27525b441ab 100644
--- a/app/views/projects/branch_rules/_show.html.haml
+++ b/app/views/projects/branch_rules/_show.html.haml
@@ -9,4 +9,4 @@
= _('Define rules for who can push, merge, and the required approvals for each branch.')
.settings-content.gl-pr-0
- #js-branch-rules{ data: { project_path: @project.full_path } }
+ #js-branch-rules{ data: { project_path: @project.full_path, branch_rules_path: project_settings_repository_branch_rules_path(@project) } }
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index 10a6bc6b524..34aecd31c57 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -2,17 +2,17 @@
- dropdown_class = local_assigns.fetch(:dropdown_class, '')
.git-clone-holder.js-git-clone-holder
- %a#clone-dropdown.gl-button.btn.btn-confirm.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
+ %a#clone-dropdown.gl-button.btn.btn-confirm.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } }
%span.gl-mr-2.js-clone-dropdown-label
= _('Clone')
= sprite_icon("chevron-down", css_class: "icon")
- %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options{ class: dropdown_class }
+ %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown{ class: dropdown_class, data: { qa_selector: 'clone_dropdown_content' } }
- if ssh_enabled?
%li{ class: 'gl-px-4!' }
%label.label-bold
= _('Clone with SSH')
.input-group.btn-group
- = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: _('Repository clone URL') }
+ = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'ssh_clone_url_content' }
.input-group-append
= clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
= render_if_exists 'projects/buttons/geo'
@@ -21,7 +21,7 @@
%label.label-bold
= _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
.input-group.btn-group
- = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control qa-http-clone-url", readonly: true, aria: { label: _('Repository clone URL') }
+ = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'http_clone_url_content' }
.input-group-append
= clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text gl-button btn btn-icon btn-default")
= render_if_exists 'projects/buttons/geo'
diff --git a/app/views/projects/buttons/_download_links.html.haml b/app/views/projects/buttons/_download_links.html.haml
index f6084cfcde8..d36aed44e18 100644
--- a/app/views/projects/buttons/_download_links.html.haml
+++ b/app/views/projects/buttons/_download_links.html.haml
@@ -1,4 +1,4 @@
.btn-group.ml-0.w-100
- Gitlab::Workhorse::ARCHIVE_FORMATS.each_with_index do |fmt, index|
- archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
- = link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "gl-button btn btn-sm #{index == 0 ? "btn-confirm" : "btn-default"}"
+ = link_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', class: "gl-button btn btn-sm #{index == 0 ? 'btn-confirm' : 'btn-default'}"
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index c57b6dbe28c..3621853430d 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -14,5 +14,5 @@
= link_to new_project_fork_path(@project), class: "gl-button btn btn-default btn-sm fork-btn #{button_class}" do
= sprite_icon('fork', css_class: 'icon')
%span= s_('ProjectOverview|Fork')
- = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Forks'), s_('ProjectOverview|Forks'), @project.forks_count), class: "gl-button btn btn-default btn-sm count has-tooltip #{count_class}" do
+ = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Forks'), s_('ProjectOverview|Forks'), @project.forks_count), class: "gl-button btn btn-default btn-sm count has-tooltip fork-count #{count_class}" do
= @project.forks_count
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index f607a21ad21..eaf906ad89f 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,15 +1,13 @@
- if current_user
+ - starred = current_user.starred?(@project)
+ - icon = starred ? 'star' : 'star-o'
+ - button_text = starred ? s_('ProjectOverview|Unstar') : s_('ProjectOverview|Star')
+ - button_text_classes = starred ? 'starred' : ''
.count-badge.d-inline-flex.align-item-stretch.gl-mr-3.btn-group
- %button.gl-button.btn.btn-default.btn-sm.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }
- - if current_user.starred?(@project)
- = sprite_icon('star', css_class: 'icon')
- %span.starred= s_('ProjectOverview|Unstar')
- - else
- = sprite_icon('star-o', css_class: 'icon')
- %span= s_('ProjectOverview|Star')
+ = render Pajamas::ButtonComponent.new(size: :small, icon: icon, button_text_classes: button_text_classes, button_options: { class: 'star-btn toggle-star', data: { endpoint: toggle_star_project_path(@project, :json) } }) do
+ - button_text
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm has-tooltip star-count count' do
= @project.star_count
-
- else
.count-badge.d-inline-flex.align-item-stretch.gl-mr-3.btn-group
= link_to new_user_session_path, class: 'gl-button btn btn-default btn-sm has-tooltip star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index c53205b6c58..d00d9f62999 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -16,7 +16,7 @@
.settings-content
- url = cleanup_namespace_project_settings_repository_path(@project.namespace, @project)
- = form_for @project, url: url, method: :post, authenticity_token: true, html: { class: 'js-requires-input' } do |f|
+ = gitlab_ui_form_for @project, url: url, method: :post, authenticity_token: true, html: { class: 'js-requires-input' } do |f|
%fieldset.gl-mt-0.gl-mb-3
.gl-mb-3
%h5.gl-mt-0
@@ -29,4 +29,4 @@
.form-text.text-muted
= _("The maximum file size is %{size}.") % { size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) }
- = f.submit _('Start cleanup'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Start cleanup'), pajamas_button: true
diff --git a/app/views/projects/cluster_agents/show.html.haml b/app/views/projects/cluster_agents/show.html.haml
index a2d3426d99c..98a2c9c3e6d 100644
--- a/app/views/projects/cluster_agents/show.html.haml
+++ b/app/views/projects/cluster_agents/show.html.haml
@@ -1,3 +1,4 @@
+- add_page_specific_style 'page_bundles/cluster_agents'
- add_to_breadcrumbs _('Kubernetes'), project_clusters_path(@project)
- page_title @agent_name
diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml
index fb31ac44118..978d83bf2b4 100644
--- a/app/views/projects/commit/_signature.html.haml
+++ b/app/views/projects/commit/_signature.html.haml
@@ -1,3 +1,3 @@
- if signature
- - uri = "projects/commit/#{"x509/" if x509_signature?(signature)}"
+ - uri = "projects/commit/#{'x509/' if x509_signature?(signature)}"
= render partial: "#{uri}#{signature.verification_status}_signature_badge", locals: { signature: signature }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 7c896cd71ef..fb30bfc2953 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -17,7 +17,7 @@
- content = capture do
- if show_user
.clearfix
- - uri_signature_badge_user = "projects/commit/#{"x509/" if x509_signature?(signature)}signature_badge_user"
+ - uri_signature_badge_user = "projects/commit/#{'x509/' if x509_signature?(signature)}signature_badge_user"
= render partial: "#{uri_signature_badge_user}", locals: { signature: signature }
- if x509_signature?(signature)
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 6f44c130603..bf6b628dd36 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -29,7 +29,7 @@
- if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: ["commit-row-message item-title js-onboarding-commit-item", ("font-italic" if commit.message.empty?)]
- else
- = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title js-onboarding-commit-item #{"font-italic" if commit.message.empty?}", data: link_data_attrs)
+ = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title js-onboarding-commit-item #{'font-italic' if commit.message.empty?}", data: link_data_attrs)
%span.commit-row-message.d-inline.d-sm-none
&middot;
= commit.short_id
@@ -51,7 +51,7 @@
= render_if_exists 'projects/commits/project_namespace', show_project_name: show_project_name, project: project
- if commit.description?
- %pre{ class: ["commit-row-description gl-mb-3", (collapsible ? "js-toggle-content" : "d-block")] }
+ %pre{ class: ["commit-row-description gl-mb-3 gl-white-space-pre-line", (collapsible ? "js-toggle-content" : "d-block")] }
= preserve(markdown_field(commit, :description))
.commit-actions.flex-row
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 6b06584ea25..ae68a13929e 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -1,5 +1,5 @@
- breadcrumb_title _("Commits")
-
+- add_page_specific_style 'page_bundles/tree'
- page_title _("Commits"), @ref
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_commits_path(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 95186b85838..1bdf3d1e6e3 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -5,7 +5,7 @@
.js-signature-container{ data: { 'signatures-path' => signatures_namespace_project_compare_index_path } }
#js-compare-selector{ data: project_compare_selector_data(@project, @merge_request, params) }
-- if @commits.present?
+- if @commits.present? || @diffs.present?
-# Only show commit list in the first page
- hide_commit_list = params[:page].present? && params[:page] != '1'
= render "projects/commits/commit_list" unless hide_commit_list
diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml
index eba0f336f80..04712cd59f7 100644
--- a/app/views/projects/default_branch/_show.html.haml
+++ b/app/views/projects/default_branch/_show.html.haml
@@ -17,7 +17,7 @@
- else
.form-group
= f.label :default_branch, _("Default branch"), class: 'label-bold'
- = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide', data: { qa_selector: 'default_branch_dropdown' }})
+ .js-select-default-branch{ data: { default_branch: @project.default_branch, project_id: @project.id } }
.form-group
- help_text = _("When merge requests and commits in the default branch close, any issues they reference also close.")
@@ -26,4 +26,4 @@
_("Auto-close referenced issues on default branch"),
help_text: (help_text + "&nbsp;" + help_icon).html_safe
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml
index 04e364d6b15..91444a00334 100644
--- a/app/views/projects/deploy_keys/edit.html.haml
+++ b/app/views/projects/deploy_keys/edit.html.haml
@@ -6,5 +6,5 @@
= gitlab_ui_form_for [@project, @deploy_key], include_id: false, html: { class: 'js-requires-input' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
= link_to _('Cancel'), project_settings_repository_path(@project), class: 'gl-button btn btn-default btn-cancel'
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index e92297a5a6a..e3688c8d323 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -23,7 +23,7 @@
- if deployment.deployable
.table-mobile-content
.flex-truncate-parent
- .flex-truncate-child
+ .flex-truncate-child.has-tooltip.gl-white-space-normal.gl-md-white-space-nowrap{ :title => "#{deployment.deployable.name} (##{deployment.deployable.id})", data: { container: 'body' } }
= link_to deployment_path(deployment), class: 'build-link' do
#{deployment.deployable.name} (##{deployment.deployable.id})
- else
diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml
index a7befabdc96..223f7520b47 100644
--- a/app/views/projects/deployments/_rollback.haml
+++ b/app/views/projects/deployments/_rollback.haml
@@ -1,7 +1,4 @@
- if deployment.deployable && can?(current_user, :create_deployment, deployment)
- tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment')
- = button_tag class: 'js-confirm-rollback-modal-button gl-button btn btn-default btn-icon has-tooltip', type: 'button', data: { environment_name: @environment.name, commit_short_sha: deployment.short_sha, commit_url: project_commit_path(@project, deployment.sha), is_last_deployment: deployment.last?.to_s, retry_path: retry_project_job_path(@environment.project, deployment.deployable) }, title: tooltip do
- - if deployment.last?
- = sprite_icon('repeat', css_class: 'gl-icon')
- - else
- = sprite_icon('redo', css_class: 'gl-icon')
+ - icon = deployment.last? ? 'repeat' : 'redo'
+ = render Pajamas::ButtonComponent.new(icon: icon, button_options: { title: tooltip, class: 'js-confirm-rollback-modal-button has-tooltip', data: { environment_name: @environment.name, commit_short_sha: deployment.short_sha, commit_url: project_commit_path(@project, deployment.sha), is_last_deployment: deployment.last?.to_s, retry_path: retry_project_job_path(@environment.project, deployment.deployable) } })
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 70df995cdf3..f6e3c15c08b 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -28,7 +28,7 @@
%template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
.js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
-- if show_merge_request_settings_callout?
+- if show_merge_request_settings_callout?(@project)
%section.settings.expanded
= render Pajamas::AlertComponent.new(variant: :info,
title: _('Merge requests and approvals settings have moved.'),
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index cd7339edd1a..31041d124e4 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -1,3 +1,5 @@
+- add_page_specific_style 'page_bundles/prometheus'
+
- page_title _("Metrics Dashboard"), @environment.name
.prometheus-container
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
index 2e024b8ffc4..7cd4ab08680 100644
--- a/app/views/projects/find_file/show.html.haml
+++ b/app/views/projects/find_file/show.html.haml
@@ -1,4 +1,5 @@
- page_title _("Find File"), @ref
+- add_page_specific_style 'page_bundles/tree'
.file-finder-holder.tree-holder.clearfix.js-file-finder{ 'data-file-find-url': "#{escape_javascript(project_files_path(@project, @ref, format: :json))}", 'data-find-tree-url': escape_javascript(project_tree_path(@project, @ref)), 'data-blob-url-template': escape_javascript(project_blob_path(@project, @ref)) }
.nav-block
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index 04d400688d4..edf8f71c673 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -1,4 +1,5 @@
- page_title _("Repository Analytics")
+- add_page_specific_style 'page_bundles/graph_charts'
.mb-3
%h3
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index b350455807d..ca71990f5e3 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -12,7 +12,7 @@
= gitlab_ui_form_for [@project, @hook], as: :hook, url: project_hook_path(@project, @hook) do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
- = f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mr-3'
+ = f.submit _('Save changes'), pajamas_button: true
= render 'shared/web_hooks/test_button', hook: @hook
= link_to _('Delete'), project_hook_path(@project, @hook), method: :delete, class: 'btn gl-button btn-danger float-right', aria: { label: s_('Webhooks|Delete webhook') }, data: { confirm: s_('Webhooks|Are you sure you want to delete this project hook?'), confirm_btn_variant: 'danger' }
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index 7d62a851aa1..0476193c2cb 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -2,13 +2,13 @@
- breadcrumb_title _('Webhook Settings')
- page_title _('Webhooks')
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4
= render 'shared/web_hooks/title_and_docs', hook: @hook
.col-lg-8.gl-mb-3
= gitlab_ui_form_for @hook, as: :hook, url: polymorphic_path([@project, :hooks]) do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
- = f.submit 'Add webhook', class: 'gl-button btn btn-confirm'
+ = f.submit 'Add webhook', pajamas_button: true
= render 'shared/web_hooks/index', hooks: @hooks, hook_class: @hook.class
diff --git a/app/views/projects/incidents/show.html.haml b/app/views/projects/incidents/show.html.haml
index 5043f94bd5c..7a1e7f503f8 100644
--- a/app/views/projects/incidents/show.html.haml
+++ b/app/views/projects/incidents/show.html.haml
@@ -2,6 +2,7 @@
- add_to_breadcrumbs _("Incidents"), project_incidents_path(@project)
- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Incidents")
+- add_page_specific_style 'page_bundles/incidents'
- add_page_specific_style 'page_bundles/issues_show'
= render 'projects/issuable/show', issuable: @issue
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 11b652cc818..40935ab6f70 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -6,6 +6,9 @@
#js-vue-notes{ data: { notes_data: notes_data(@issue).to_json,
noteable_data: serialize_issuable(@issue, with_blocking_issues: true),
noteable_type: 'Issue',
+ notes_filters: UserPreference.notes_filters.to_json,
+ notes_filter_value: current_user&.notes_filter_for(@issue),
target_type: 'issue',
+ show_timeline_view_toggle: show_timeline_view_toggle?(@issue).to_s,
current_user_data: UserSerializer.new.represent(current_user, {only_path: true}, CurrentUserEntity).to_json,
can_add_timeline_events: "#{can?(current_user, :admin_incident_management_timeline_event, @issue)}" } }
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 4d4645c7087..1d3320e4f82 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,10 +1,5 @@
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id, qa_selector: 'issue_container', qa_issue_title: issue.title } }
.issuable-info-container
- - if @can_bulk_update
- .issue-check.hidden
- - checkbox_id = dom_id(issue, "selected")
- %label.gl-sr-only{ for: checkbox_id }= issue.title
- = check_box_tag checkbox_id, nil, false, 'data-id' => issue.id, class: "selected-issuable"
.issuable-main-info
.issue-title.title
%span.issue-title-text.js-onboarding-issue-item{ dir: "auto" }
diff --git a/app/views/projects/issues/_work_item_links.html.haml b/app/views/projects/issues/_work_item_links.html.haml
index bc2136b89fb..c0de711136a 100644
--- a/app/views/projects/issues/_work_item_links.html.haml
+++ b/app/views/projects/issues/_work_item_links.html.haml
@@ -1,2 +1,2 @@
- if Feature.enabled?(:work_items_hierarchy, @project)
- .js-work-item-links-root{ data: { issuable_id: @issue.id, iid: @issue.iid, project_path: @project.full_path, wi: work_items_index_data(@project) } }
+ .js-work-item-links-root{ data: { issuable_id: @issue.id, iid: @issue.iid, project_namespace: @project.namespace.path, project_path: @project.full_path, wi: work_items_index_data(@project) } }
diff --git a/app/views/projects/issues/service_desk/_service_desk_info_content.html.haml b/app/views/projects/issues/service_desk/_service_desk_info_content.html.haml
index bad75ac2cd9..2ed5675c0ad 100644
--- a/app/views/projects/issues/service_desk/_service_desk_info_content.html.haml
+++ b/app/views/projects/issues/service_desk/_service_desk_info_content.html.haml
@@ -4,7 +4,7 @@
- can_admin_issues = can?(current_user, :admin_issue, @project)
- title_text = s_("ServiceDesk|Use Service Desk to connect with your users and offer customer support through email right inside GitLab")
-.non-empty-state.media
+.media.gl-border-b.gl-pb-3.gl-text-left
.svg-content
= render partial: 'shared/empty_states/icons/service_desk_callout', formats: :svg
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 06c422fc4d6..76b725d140c 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -2,6 +2,7 @@
- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
+- add_page_specific_style 'page_bundles/incidents'
- add_page_specific_style 'page_bundles/issues_show'
- add_page_specific_style 'page_bundles/work_items'
diff --git a/app/views/projects/jobs/_user.html.haml b/app/views/projects/jobs/_user.html.haml
index 90ce581a903..03cbabb0c2a 100644
--- a/app/views/projects/jobs/_user.html.haml
+++ b/app/views/projects/jobs/_user.html.haml
@@ -1,7 +1,7 @@
by
%a{ href: user_path(@build.user) }
%span.d-none.d-sm-inline
- = image_tag avatar_icon_for_user(@build.user, 24), class: "avatar s24"
+ = render Pajamas::AvatarComponent.new(@build.user, size: 24, alt: "")
%strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } }
= @build.user.name
%strong.d-inline.d-sm-none= @build.user.to_reference
diff --git a/app/views/projects/merge_requests/_awards_block.html.haml b/app/views/projects/merge_requests/_awards_block.html.haml
index 64d35b4dfe6..820927fdd1a 100644
--- a/app/views/projects/merge_requests/_awards_block.html.haml
+++ b/app/views/projects/merge_requests/_awards_block.html.haml
@@ -1,5 +1,2 @@
.content-block.emoji-block.emoji-list-container.js-noteable-awards
- = render 'award_emoji/awards_block', awardable: @merge_request, inline: true, api_awards_path: award_emoji_merge_request_api_path(@merge_request) do
- .gl-my-2.gl-xs-w-full
- #js-vue-sort-issue-discussions
- = render "projects/merge_requests/discussion_filter"
+ = render 'award_emoji/awards_block', awardable: @merge_request, inline: true, api_awards_path: award_emoji_merge_request_api_path(@merge_request)
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
index 22571b11639..478db70877d 100644
--- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -1,8 +1,7 @@
- display_issuable_type = issuable_display_type(@merge_request)
.float-left.btn-group.gl-md-ml-3.gl-display-flex.dropdown.gl-new-dropdown.gl-md-w-auto.gl-w-full
- = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret gl-display-none! gl-md-display-inline-flex!", data: { 'toggle' => 'dropdown' } do
- %span.gl-sr-only= _('Toggle dropdown')
+ = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret has-tooltip gl-display-none! gl-md-display-inline-flex!", data: { toggle: 'dropdown', title: _('Merge request actions'), testid: 'merge-request-actions' } do
= sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon"
= button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do
%span.gl-new-dropdown-button-text= _('Merge request actions')
@@ -11,7 +10,7 @@
.gl-new-dropdown-inner
.gl-new-dropdown-contents
%ul
- - if !@merge_request.merged? && current_user && moved_mr_sidebar_enabled?
+ - if current_user && moved_mr_sidebar_enabled?
%li.gl-new-dropdown-item.js-sidebar-subscriptions-entry-point
%li.gl-new-dropdown-divider
%hr.dropdown-divider
diff --git a/app/views/projects/merge_requests/_discussion_filter.html.haml b/app/views/projects/merge_requests/_discussion_filter.html.haml
deleted file mode 100644
index 96886661a8d..00000000000
--- a/app/views/projects/merge_requests/_discussion_filter.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request),
- notes_filters: UserPreference.notes_filters.to_json } }
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 98d2928fc97..71f8e4c32f5 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,9 +1,12 @@
%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
- if @can_bulk_update
- .issue-check.hidden
- - checkbox_id = dom_id(merge_request, "selected")
- %label.gl-sr-only{ for: checkbox_id }= merge_request.title
- = check_box_tag checkbox_id, nil, false, 'data-id' => merge_request.id, class: "selected-issuable"
+ .issue-check.gl-mr-3.hidden
+ = render Pajamas::CheckboxTagComponent.new(name: dom_id(merge_request, "selected"),
+ value: nil,
+ checkbox_options: { 'data-id' => merge_request.id }) do |c|
+ = c.label do
+ %span.gl-sr-only
+ = merge_request.title
.issuable-info-container
.issuable-main-info
diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml
index 00d12423eb9..1efea6a1d37 100644
--- a/app/views/projects/merge_requests/_nav_btns.html.haml
+++ b/app/views/projects/merge_requests/_nav_btns.html.haml
@@ -5,7 +5,8 @@
.js-csv-import-export-buttons{ data: { show_export_button: "true", issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_merge_requests_path(@project, request.query_parameters), container_class: 'gl-mr-3' } }
- if @can_bulk_update
- = button_tag _("Edit merge requests"), class: "gl-button btn btn-default gl-mr-3 js-bulk-update-toggle"
+ = render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-mr-3 js-bulk-update-toggle' }) do
+ = _("Edit merge requests")
- if merge_project
- = link_to new_merge_request_path, class: "gl-button btn btn-confirm", title: _("New merge request") do
- = _('New merge request')
+ = render Pajamas::ButtonComponent.new(href: new_merge_request_path, variant: :confirm) do
+ = _("New merge request")
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index 783e3ac97c1..4f6983c6fe3 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -12,6 +12,7 @@
window.gl.mrWidgetData.mr_troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviews/index.md', anchor: 'troubleshooting')}';
window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'require-a-successful-pipeline-for-merge')}';
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.md', anchor: 'security-approvals-in-merge-requests')}';
+ window.gl.mrWidgetData.security_configuration_path = '#{project_security_configuration_path(@project)}';
window.gl.mrWidgetData.license_compliance_docs_path = '#{help_page_path('user/compliance/license_compliance/index.md', anchor: 'policies')}';
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/approvals/rules.md', anchor: 'eligible-approvers')}';
window.gl.mrWidgetData.approvals_help_path = '#{help_page_path("user/project/merge_requests/approvals/index.md")}';
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index d34848c801d..d77d5231a7d 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -60,10 +60,10 @@
%section
.issuable-discussion.js-vue-notes-event
- if @merge_request.description.present?
- .detail-page-description
+ .detail-page-description.gl-pb-0
= render "projects/merge_requests/description"
- = render "projects/merge_requests/widget"
= render "projects/merge_requests/awards_block"
+ = render "projects/merge_requests/widget"
- if mr_action === "show"
- add_page_startup_api_call Feature.enabled?(:paginated_mr_discussions, @project) ? discussions_path(@merge_request, per_page: 20) : discussions_path(@merge_request)
- add_page_startup_api_call widget_project_json_merge_request_path(@project, @merge_request, format: :json)
@@ -72,6 +72,8 @@
endpoint_metadata: @endpoint_metadata_url,
noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'),
noteable_type: 'MergeRequest',
+ notes_filters: UserPreference.notes_filters.to_json,
+ notes_filter_value: current_user&.notes_filter_for(@merge_request),
target_type: 'merge_request',
help_page_path: suggest_changes_help_path,
current_user_data: @current_user_data,
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index c11d5e7c9b6..fb7c1130f5c 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,6 +1,8 @@
= gitlab_ui_form_for [@project, @milestone],
html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
= form_errors(@milestone)
+ - if @redirect_path.present?
+ = f.hidden_field(:redirect_path, name: :redirect_path, id: :redirect_path, value: @redirect_path)
.form-group.row
.col-form-label.col-sm-2
= f.label :title, _('Title')
diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml
index 28b433b2514..4b549aaf1cd 100644
--- a/app/views/projects/mirrors/_authentication_method.html.haml
+++ b/app/views/projects/mirrors/_authentication_method.html.haml
@@ -3,12 +3,10 @@
.form-group
= f.label :auth_method, _('Authentication method'), class: 'label-bold'
- .select-wrapper
- = f.select :auth_method,
- options_for_select(auth_options, mirror.auth_method),
- {}, { class: "form-control gl-form-select select-control js-mirror-auth-type qa-authentication-method" }
- = sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
- = f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type"
+ = f.select :auth_method,
+ options_for_select(auth_options, mirror.auth_method),
+ {}, { class: "custom-select gl-form-select js-mirror-auth-type qa-authentication-method" }
+ = f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type"
.form-group
.well-password-auth.collapse.js-well-password-auth
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 2ae7d300979..c98f88fa31e 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -35,7 +35,7 @@
= link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer'
.panel-footer
- = f.submit _('Mirror repository'), class: 'gl-button btn btn-confirm js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
+ = f.submit _('Mirror repository'), class: 'js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror, pajamas_button: true
- else
= render Pajamas::AlertComponent.new(dismissible: false) do |c|
= c.body do
diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml
index 701cb37a1c8..e430dc2f372 100644
--- a/app/views/projects/network/_head.html.haml
+++ b/app/views/projects/network/_head.html.haml
@@ -1,4 +1,4 @@
-.row-content-block.second-block.content-component-block
+.row-content-block.second-block.content-component-block.gl-px-0.gl-py-3
.tree-ref-holder
= render partial: 'shared/ref_switcher', locals: {destination: 'graph'}
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
index 993026d2884..ff30c9ce1ea 100644
--- a/app/views/projects/pages/_destroy.haml
+++ b/app/views/projects/pages/_destroy.haml
@@ -1,14 +1,16 @@
- if @project.pages_deployed?
- if can?(current_user, :remove_pages, @project)
- .card.border-danger
- .card-header.bg-danger.text-white
+ = render Pajamas::CardComponent.new(card_options: { class: 'border-danger' }, header_options: {class: 'bg-danger text-white'}) do |c|
+ - c.with_header do
= s_('GitLabPages|Remove pages')
- .errors-holder
- .card-body
- %p.gl-mb-0
- = s_('GitLabPages|Removing pages will prevent them from being exposed to the outside world.')
- .card-footer
- = link_to s_('GitLabPages|Remove pages'), project_pages_path(@project), data: { confirm: s_('GitLabPages|Are you sure?'), 'confirm-btn-variant': 'danger'}, method: :delete, class: "btn gl-button btn-danger", "aria-label": s_('GitLabPages|Remove pages')
+ - c.with_body do
+ = s_('GitLabPages|Removing pages will prevent them from being exposed to the outside world.')
+ - c.with_footer do
+ = render Pajamas::ButtonComponent.new(href: project_pages_path(@project),
+ variant: :danger,
+ method: :delete,
+ button_options: {data: { confirm: s_('GitLabPages|Are you sure?'), 'confirm-btn-variant': 'danger'}, "aria-label": s_('GitLabPages|Remove pages')}) do
+ = s_('GitLabPages|Remove pages')
- else
.nothing-here-block
= s_('GitLabPages|Only project maintainers can remove pages')
diff --git a/app/views/projects/pages/new.html.haml b/app/views/projects/pages/new.html.haml
index 5dea6b02e36..f1f3510d0f8 100644
--- a/app/views/projects/pages/new.html.haml
+++ b/app/views/projects/pages/new.html.haml
@@ -1,7 +1,8 @@
-- if Feature.enabled?(:use_pipeline_wizard_for_pages, @project.group)
- #js-pages{ data: @pipeline_wizard_data }
+%section.js-search-settings-section
+ - if Feature.enabled?(:use_pipeline_wizard_for_pages, @project.group)
+ #js-pages{ data: @pipeline_wizard_data }
-- else
- = render 'header'
+ - else
+ = render 'header'
- = render 'use'
+ = render 'use'
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index 5d5ca2aaaf3..ab692d1830a 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -39,5 +39,5 @@
%div
= f.gitlab_ui_checkbox_component :active, _('Active'), checkbox_options: { value: @schedule.active, required: false }
.footer-block
- = f.submit _('Save pipeline schedule'), class: 'btn gl-button btn-confirm'
+ = f.submit _('Save pipeline schedule'), pajamas_button: true
= link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn gl-button btn-default btn-cancel'
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 10dc74647b2..7b16564dfa2 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -4,9 +4,9 @@
= pipeline_schedule.description
%td.branch-name-cell.gl-text-truncate
- if pipeline_schedule.for_tag?
- = sprite_icon('tag', size: 12)
+ = sprite_icon('tag', size: 12, css_class: 'gl-vertical-align-middle!' )
- else
- = sprite_icon('fork', size: 12)
+ = sprite_icon('fork', size: 12, css_class: 'gl-vertical-align-middle!')
- if pipeline_schedule.ref.present?
= link_to pipeline_schedule.ref_for_display, project_ref_path(@project, pipeline_schedule.ref_for_display), class: "ref-name"
%td
@@ -24,7 +24,7 @@
= s_("PipelineSchedules|Inactive")
%td
- if pipeline_schedule.owner
- = image_tag avatar_icon_for_user(pipeline_schedule.owner, 20), class: "avatar s20"
+ = render Pajamas::AvatarComponent.new(pipeline_schedule.owner, size: 24, class: "gl-mr-2")
= link_to user_path(pipeline_schedule.owner) do
= pipeline_schedule.owner&.name
%td
diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml
index 642b458eea6..3f843ce6aec 100644
--- a/app/views/projects/pipeline_schedules/edit.html.haml
+++ b/app/views/projects/pipeline_schedules/edit.html.haml
@@ -7,4 +7,7 @@
= _("Edit Pipeline Schedule")
%hr
-= render "form"
+- if Feature.enabled?(:pipeline_schedules_vue, @project)
+ #pipeline-schedules-form-edit{ data: { full_path: @project.full_path } }
+- else
+ = render "form"
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 661cf465081..47ad8cc826d 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -1,22 +1,28 @@
- breadcrumb_title _("Schedules")
- page_title _("Pipeline Schedules")
- add_page_specific_style 'page_bundles/pipeline_schedules'
+- add_page_specific_style 'page_bundles/ci_status'
#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules'), illustration_url: image_path('illustrations/pipeline_schedule_callout.svg') } }
-.top-area
- - schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) }
- = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
- - if can?(current_user, :create_pipeline_schedule, @project)
- .nav-controls
- = link_to new_project_pipeline_schedule_path(@project), class: 'btn gl-button btn-confirm' do
- %span= _('New schedule')
-
-- if @schedules.present?
- %ul.content-list
- = render partial: "table"
+- if Feature.enabled?(:pipeline_schedules_vue, @project)
+ #pipeline-schedules-app{ data: { full_path: @project.full_path } }
- else
- .card.bg-light.gl-mt-3
- .nothing-here-block= _("No schedules")
+ .top-area
+ - schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) }
+ = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
+
+ - if can?(current_user, :create_pipeline_schedule, @project)
+ .nav-controls
+ = link_to new_project_pipeline_schedule_path(@project), class: 'btn gl-button btn-confirm' do
+ %span= _('New schedule')
+
+ - if @schedules.present?
+ %ul.content-list
+ = render partial: "table"
+ - else
+ = render Pajamas::CardComponent.new(card_options: { class: 'bg-light gl-mt-3 gl-text-center' }) do |c|
+ - c.body do
+ = _("No schedules")
-#pipeline-take-ownership-modal
+ #pipeline-take-ownership-modal
diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml
index 3b4acf5b8c5..d3757d0e339 100644
--- a/app/views/projects/pipeline_schedules/new.html.haml
+++ b/app/views/projects/pipeline_schedules/new.html.haml
@@ -8,4 +8,7 @@
%h1.page-title.gl-font-size-h-display
= _("Schedule a new pipeline")
-= render "form"
+- if Feature.enabled?(:pipeline_schedules_vue, @project)
+ #pipeline-schedules-form-new{ data: { full_path: @project.full_path } }
+- else
+ = render "form"
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 07e299d71ea..2e403358e2e 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -9,18 +9,21 @@
.well-segment.pipeline-info{ class: "gl-align-items-baseline!" }
.icon-container
= sprite_icon('clock', css_class: 'gl-top-0!')
- = pluralize @pipeline.total_size, "job"
- = @pipeline.ref_text
+ - jobs = n_('%d job', '%d jobs', @pipeline.total_size) % @pipeline.total_size
- if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
+ = s_('Pipelines|%{jobs} %{ref_text} in %{duration}').html_safe % { jobs: jobs, ref_text: @pipeline.ref_text, duration: time_interval_in_words(@pipeline.duration) }
+ - else
+ = jobs
+ = @pipeline.ref_text
- if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+ = s_("Pipelines|(queued for %{queued_duration})") % { queued_duration: time_interval_in_words(@pipeline.queued_duration)}
- if has_pipeline_badges?(@pipeline)
.well-segment.qa-pipeline-badges
.icon-container
= sprite_icon('flag', css_class: 'gl-top-0!')
+ - if @pipeline.schedule?
+ = gl_badge_tag _('Scheduled'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-scheduled', title: _('This pipeline was triggered by a schedule.') }
- if @pipeline.child?
- text = sprintf(s_('Pipelines|Child pipeline (%{link_start}parent%{link_end})'), { link_start: "<a href='#{pipeline_path(@pipeline.triggered_by_pipeline)}' class='text-underline'>", link_end: "</a>"}).html_safe
= gl_badge_tag text, { variant: :info, size: :sm }, { class: 'js-pipeline-child has-tooltip', title: s_("Pipelines|This is a child pipeline within the parent pipeline") }
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index a4144f8ab0d..d2b2a58fcf8 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -12,6 +12,7 @@
ref_param: params[:ref] || @project.default_branch,
var_param: params[:var].to_json,
file_param: params[:file_var].to_json,
+ project_path: @project.full_path,
project_refs_endpoint: refs_project_path(@project, sort: 'updated_desc'),
settings_link: project_settings_ci_cd_path(@project),
max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
diff --git a/app/views/projects/project_templates/_project_fields_form.html.haml b/app/views/projects/project_templates/_project_fields_form.html.haml
index 7908550ca88..c3528b421b9 100644
--- a/app/views/projects/project_templates/_project_fields_form.html.haml
+++ b/app/views/projects/project_templates/_project_fields_form.html.haml
@@ -8,5 +8,5 @@
.selected-icon.gl-mr-3
.selected-template
.input-group-append
- %button.btn.gl-button.btn-default.change-template{ type: "button" }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'change-template' }) do
= _('Change template')
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index 34fe9a29068..76aadc3be28 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -1,12 +1,12 @@
- content_for :merge_access_levels do
.merge_access_levels-container
- = dropdown_tag('Select',
+ = dropdown_tag(_('Select'),
options: { toggle_class: 'js-allowed-to-merge wide',
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_merge_dropdown_content', dropdown_testid: 'allowed-to-merge-dropdown',
data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes', qa_selector: 'allowed_to_merge_dropdown' }})
- content_for :push_access_levels do
.push_access_levels-container
- = dropdown_tag('Select',
+ = dropdown_tag(_('Select'),
options: { toggle_class: "js-allowed-to-push js-multiselect wide",
dropdown_class: 'dropdown-menu-selectable capitalize-header', dropdown_qa_selector: 'allowed_to_push_dropdown_content' , dropdown_testid: 'allowed-to-push-dropdown',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes', qa_selector: 'allowed_to_push_dropdown' }})
diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
index 277cbf00034..770d79943b3 100644
--- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f|
+= gitlab_ui_form_for [@project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f|
%input{ type: 'hidden', name: 'update_section', value: 'js-protected-branches-settings' }
= render Pajamas::CardComponent.new(card_options: { class: "gl-mb-5" }) do |c|
- c.header do
@@ -32,4 +32,4 @@
= (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe
= render_if_exists 'projects/protected_branches/ee/code_owner_approval_form', f: f
- c.footer do
- = f.submit s_('ProtectedBranch|Protect'), class: 'gl-button btn btn-confirm', disabled: true, data: { qa_selector: 'protect_button' }
+ = f.submit s_('ProtectedBranch|Protect'), disabled: true, data: { qa_selector: 'protect_button' }, pajamas_button: true
diff --git a/app/views/projects/protected_branches/shared/_dropdown.html.haml b/app/views/projects/protected_branches/shared/_dropdown.html.haml
index 4b09d36e7c3..d5111bd8be5 100644
--- a/app/views/projects/protected_branches/shared/_dropdown.html.haml
+++ b/app/views/projects/protected_branches/shared/_dropdown.html.haml
@@ -2,11 +2,11 @@
= f.hidden_field(:name)
-= dropdown_tag('Select branch or create wildcard',
+= dropdown_tag(_('Select branch or create wildcard'),
options: { toggle_class: "js-protected-branch-select js-filter-submit wide monospace qa-protected-branch-select #{toggle_classes}",
filter: true,
dropdown_class: "dropdown-menu-selectable git-revision-dropdown qa-protected-branch-dropdown",
- placeholder: "Search protected branches",
+ placeholder: _("Search protected branches"),
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_branch_name],
@@ -14,6 +14,6 @@
%ul.dropdown-footer-list
%li
- %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: "New Protected Branch" }
- Create wildcard
+ %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: _("New Protected Branch") }
+ = _('Create wildcard')
%code
diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
index ba0935fff7d..9ea7f397c0a 100644
--- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
@@ -1,9 +1,9 @@
= form_for [@project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f|
%input{ type: 'hidden', name: 'update_section', value: 'js-protected-tags-settings' }
- .card
- .card-header
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }) do |c|
+ - c.header do
= _('Protect a tag')
- .card-body
+ - c.body do
= form_errors(@protected_tag)
.form-group.row
= f.label :name, _('Tag:'), class: 'col-md-2 text-left text-md-right'
@@ -19,5 +19,5 @@
.create_access_levels-container
= yield :create_access_levels
- .card-footer
+ - c.footer do
= f.submit _('Protect'), class: 'gl-button btn btn-confirm', disabled: true, data: { qa_selector: 'protect_tag_button' }
diff --git a/app/views/projects/releases/index.html.haml b/app/views/projects/releases/index.html.haml
index 9ddf2201fad..975abaefc6c 100644
--- a/app/views/projects/releases/index.html.haml
+++ b/app/views/projects/releases/index.html.haml
@@ -1,4 +1,5 @@
- page_title _('Releases')
+- add_page_specific_style 'page_bundles/releases'
- if use_startup_query_for_index_page?
- add_page_startup_graphql_call('releases/all_releases', index_page_startup_query_variables)
diff --git a/app/views/projects/releases/show.html.haml b/app/views/projects/releases/show.html.haml
index 91ee9ad70a3..66b187c8c72 100644
--- a/app/views/projects/releases/show.html.haml
+++ b/app/views/projects/releases/show.html.haml
@@ -1,5 +1,6 @@
- add_to_breadcrumbs _("Releases"), project_releases_path(@project)
- page_title @release.name
- page_description @release.description_html
+- add_page_specific_style 'page_bundles/releases'
#js-show-release-page{ data: data_for_show_page }
diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml
deleted file mode 100644
index ae0d9ab9908..00000000000
--- a/app/views/projects/repositories/_feed.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- commit = update
-%tr
- %td
- = link_to project_commits_path(@project, commit.head.name) do
- %strong
- = commit.head.name
- - if @project.root_ref?(commit.head.name)
- %span.label default
-
- %td
- %div
- = link_to project_commits_path(@project, commit.id) do
- %code= commit.short_id
- = image_tag avatar_icon_for_email(commit.author_email), class: "", width: 16, alt: ''
- = markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author)
- %td
- %span.float-right.cgray
- = time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 3df4f3a0bd0..4689e70d907 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -5,6 +5,6 @@
- if @shared_runners_count == 0
= _('This GitLab instance does not provide any shared runners yet. Instance administrators can register shared runners in the admin area.')
- else
- %h4.underlined-title #{_('Available shared runners:')} #{@shared_runners_count}
+ %h5.gl-mt-6.gl-mb-0 #{_('Available shared runners:')} #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
= render partial: 'projects/runners/runner', collection: @shared_runners, as: :runner
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
index 7ecc8004334..9f598ffb2d1 100644
--- a/app/views/projects/settings/access_tokens/index.html.haml
+++ b/app/views/projects/settings/access_tokens/index.html.haml
@@ -4,7 +4,7 @@
- type_plural = _('project access tokens')
- @content_class = 'limit-container-width' unless fluid_layout
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4
%h4.gl-mt-0
= page_title
@@ -24,13 +24,11 @@
= _('You can enable project access token creation in %{link_start}group settings%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.col-lg-8
- - if @new_resource_access_token
- = render 'shared/access_tokens/created_container',
- type: type,
- new_token_value: @new_resource_access_token
+ #js-new-access-token-app{ data: { access_token_type: type } }
- if current_user.can?(:create_resource_access_tokens, @project)
= render 'shared/access_tokens/form',
+ ajax: true,
type: type,
path: project_settings_access_tokens_path(@project),
resource: @project,
@@ -39,12 +37,8 @@
access_levels: ProjectMember.permissible_access_level_roles(current_user, @project),
default_access_level: Gitlab::Access::GUEST,
prefix: :resource_access_token,
+ description_prefix: :project_access_token,
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token')
- = render 'shared/access_tokens/table',
- active_tokens: @active_resource_access_tokens,
- resource: @project,
- type: type,
- type_plural: type_plural,
- revoke_route_helper: ->(token) { revoke_namespace_project_settings_access_token_path(id: token) },
- no_active_tokens_message: _('This project has no active access tokens.')
+ #js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_resource_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true
+ } }
diff --git a/app/views/projects/settings/branch_rules/index.html.haml b/app/views/projects/settings/branch_rules/index.html.haml
index 384d504e51f..a7e80101a88 100644
--- a/app/views/projects/settings/branch_rules/index.html.haml
+++ b/app/views/projects/settings/branch_rules/index.html.haml
@@ -1,6 +1,6 @@
- add_to_breadcrumbs _('Repository Settings'), project_settings_repository_path(@project)
-- page_title _('Branch rules')
+- page_title s_('BranchRules|Branch rules details')
-%h3= _('Branch rules')
+%h3.gl-mb-5= s_('BranchRules|Branch rules details')
-#js-branch-rules{ data: { project_path: @project.full_path } }
+#js-branch-rules{ data: { project_path: @project.full_path, protected_branches_path: project_settings_repository_path(@project, anchor: 'js-protected-branches-settings'), approval_rules_path: project_settings_merge_requests_path(@project, anchor: 'js-merge-request-approval-settings') } }
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 81526685bfc..5748b4b0330 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -41,4 +41,4 @@
= form.gitlab_ui_radio_component :deploy_strategy, 'timed_incremental', (s_('CICD|Continuous deployment to production using timed incremental rollout') + ' ' + help_link_timed).html_safe
= form.gitlab_ui_radio_component :deploy_strategy, 'manual', (s_('CICD|Automatic deployment to staging, manual deployment to production') + ' ' + help_link_incremental).html_safe
- = f.submit _('Save changes'), class: "btn gl-button btn-confirm gl-mt-5", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), class: "gl-mt-5", data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/projects/settings/ci_cd/_badge.html.haml b/app/views/projects/settings/ci_cd/_badge.html.haml
index 38d8c8d26e1..99eef38827b 100644
--- a/app/views/projects/settings/ci_cd/_badge.html.haml
+++ b/app/views/projects/settings/ci_cd/_badge.html.haml
@@ -2,15 +2,15 @@
.col-lg-12
%h4
= badge.title.capitalize
- .card
- .card-header
- %b
- = badge.title.capitalize
- &middot;
- = badge.to_html
- .float-right
- = render 'shared/ref_switcher', destination: 'badges', align_right: true
- .card-body
+ = render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, header_options: { class: 'gl-display-flex gl-align-items-center' }) do |c|
+ - c.header do
+ .gl-flex-grow-1
+ %b
+ = badge.title.capitalize
+ &middot;
+ = badge.to_html
+ = render 'shared/ref_switcher', destination: 'badges', align_right: true
+ - c.body do
.row
.col-md-2.gl-text-center
Markdown
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 9419dacc16f..51d28411b30 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -77,7 +77,7 @@
= _("The maximum file size in megabytes for individual job artifacts.")
= link_to sprite_icon('question-o'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size'), target: '_blank', rel: 'noopener noreferrer'
- = f.submit _('Save changes'), class: "btn gl-button btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
%hr
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index c1df7b88352..c4f589f3f91 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -24,7 +24,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
- auto_devops_url = help_page_path('topics/autodevops/index')
- - quickstart_url = help_page_path('topics/autodevops/quick_start_guide')
+ - quickstart_url = help_page_path('topics/autodevops/cloud_deployments/auto_devops_with_gke')
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
- quickstart_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: quickstart_url }
= s_('AutoDevOps|%{auto_devops_start}Automate building, testing, and deploying%{auto_devops_end} your applications based on your continuous integration and delivery configuration. %{quickstart_start}How do I get started?%{quickstart_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe, quickstart_start: quickstart_start, quickstart_end: '</a>'.html_safe }
@@ -119,4 +119,3 @@
= link_to _('Learn more'), help_page_path('ci/secure_files/index'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
#js-ci-secure-files{ data: { project_id: @project.id, admin: can?(current_user, :admin_secure_files, @project).to_s, file_size_limit: Ci::SecureFile::FILE_SIZE_LIMIT.to_mb } }
-
diff --git a/app/views/projects/settings/integrations/index.html.haml b/app/views/projects/settings/integrations/index.html.haml
index 84635941436..2077d244b24 100644
--- a/app/views/projects/settings/integrations/index.html.haml
+++ b/app/views/projects/settings/integrations/index.html.haml
@@ -2,8 +2,9 @@
- breadcrumb_title _('Integration Settings')
- page_title _('Integrations')
-%h3= _('Integrations')
-- integrations_link_start = '<a href="%{url}">'.html_safe % { url: help_page_url('user/project/integrations/overview') }
-- webhooks_link_start = '<a href="%{url}">'.html_safe % { url: project_hooks_path(@project) }
-%p= _("%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}.").html_safe % { integrations_link_start: integrations_link_start, webhooks_link_start: webhooks_link_start, link_end: '</a>'.html_safe }
-= render 'shared/integrations/index', integrations: @integrations
+%section.js-search-settings-section
+ %h3= _('Integrations')
+ - integrations_link_start = '<a href="%{url}">'.html_safe % { url: help_page_url('user/project/integrations/index') }
+ - webhooks_link_start = '<a href="%{url}">'.html_safe % { url: project_hooks_path(@project) }
+ %p= _("%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}.").html_safe % { integrations_link_start: integrations_link_start, webhooks_link_start: webhooks_link_start, link_end: '</a>'.html_safe }
+ = render 'shared/integrations/index', integrations: @integrations
diff --git a/app/views/projects/settings/merge_requests/show.html.haml b/app/views/projects/settings/merge_requests/show.html.haml
index 886e276dea5..7dfd304e07b 100644
--- a/app/views/projects/settings/merge_requests/show.html.haml
+++ b/app/views/projects/settings/merge_requests/show.html.haml
@@ -4,7 +4,7 @@
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings.expanded{ class: [('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header
- %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests')
+ %h4= _('Merge requests')
= render_if_exists 'projects/merge_request_settings_description_text'
.settings-content
@@ -13,6 +13,7 @@
= gitlab_ui_form_for @project, url: project_settings_merge_requests_path(@project), html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
= render 'projects/merge_request_settings', form: f
- = f.submit _('Save changes'), class: "btn gl-button btn-confirm rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }
+ = f.submit _('Save changes'), class: "rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }, pajamas_button: true
= render_if_exists 'projects/settings/merge_requests/merge_request_approvals_settings', expanded: true
+= render_if_exists 'projects/settings/merge_requests/suggested_reviewers_settings', expanded: true
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index e9d1661a4f1..c7ac28fa194 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -2,6 +2,7 @@
- @content_class = "limit-container-width" unless fluid_layout
- @skip_current_level_breadcrumb = true
- add_page_specific_style 'page_bundles/project'
+- add_page_specific_style 'page_bundles/tree'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 3e6acdb130a..ddebc19be15 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -5,7 +5,7 @@
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
-.row-content-block.top-block.content-component-block
+.row-content-block.top-block.content-component-block.gl-px-0.gl-py-2
= render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)
#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true
diff --git a/app/views/projects/starrers/_starrer.html.haml b/app/views/projects/starrers/_starrer.html.haml
index e24276fcaea..c1cd2488142 100644
--- a/app/views/projects/starrers/_starrer.html.haml
+++ b/app/views/projects/starrers/_starrer.html.haml
@@ -2,8 +2,8 @@
.col-lg-3.col-md-4.col-sm-12
.card
- .card-body
- = image_tag avatar_icon_for_user(starrer.user, 40), class: "avatar s40", alt: ''
+ .card-body.gl-display-flex
+ = render Pajamas::AvatarComponent.new(starrer.user, size: 48, alt: "", class: 'gl-mr-3')
.user-info
.block-truncated
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 79fc1a64790..ed06c90efa8 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -9,6 +9,11 @@
%h1.page-title.gl-font-size-h-display
= s_('TagsPage|New Tag')
+%p.gl-text-secondary
+ - link_start = '<a href="%{url}">'.html_safe % { url: new_namespace_project_release_path }
+ - link_end = '</a>'.html_safe
+ = s_('TagsPage|Do you want to create a release with the new tag? You can do that in the %{link_start}New release page%{link_end}.').html_safe % { link_start: link_start, link_end: link_end }
+
= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "common-note-form tag-form js-quick-submit js-requires-input" do
.form-group.row
.col-sm-12
@@ -31,22 +36,7 @@
= text_area_tag :message, @message, required: false, class: 'form-control', rows: 5, data: { qa_selector: "tag_message_field" }
.form-text.text-muted
= tag_description_help_text
- .form-group.row
- .col-sm-12
- = label_tag :release_description, s_('TagsPage|Release notes'), class: 'gl-mb-0'
- .form-text.mb-3
- - link_start = '<a href="%{url}" rel="noopener noreferrer" target="_blank">'.html_safe
- - releases_page_path = project_releases_path(@project)
- - releases_page_link_start = link_start % { url: releases_page_path }
- - docs_url = help_page_path('user/project/releases/index.md', anchor: 'create-a-release')
- - docs_link_start = link_start % { url: docs_url }
- - link_end = '</a>'.html_safe
- - replacements = { releases_page_link_start: releases_page_link_start, docs_link_start: docs_link_start, link_end: link_end }
- = s_('TagsPage|Optionally, create a public Release of your project, based on this tag. Release notes are displayed on the %{releases_page_link_start}Releases%{link_end} page. %{docs_link_start}More information%{link_end}').html_safe % replacements
- = render layout: 'shared/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
- = render 'shared/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here…'), current_text: @release_description, qa_selector: 'release_notes_field'
- = render 'shared/notes/hints'
.gl-display-flex
= render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { class: 'gl-mr-3', data: { qa_selector: "create_tag_button" }, type: 'submit' }) do
= s_('TagsPage|Create tag')
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 1553eda1cfb..6d1ab80bdc5 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -1,3 +1,4 @@
+- add_page_specific_style 'page_bundles/tree'
- current_route_path = request.fullpath.match(%r{-/tree/[^/]+/(.+$)}).to_a[1]
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
- add_page_startup_graphql_call('repository/permissions', { projectPath: @project.full_path })
diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml
index 0c53ed48210..a7f29b5cbf9 100644
--- a/app/views/projects/triggers/_index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -1,11 +1,11 @@
.row.gl-mt-3.gl-mb-3
.col-lg-12
- .card
- .card-header
+ = render Pajamas::CardComponent.new do |c|
+ - c.header do
= _("Manage your project's triggers")
- .card-body
+ - c.body do
= render 'projects/triggers/form', btn_text: _('Add trigger')
- %hr
+ .gl-mb-5
- if Feature.enabled?(:ci_pipeline_triggers_settings_vue_ui, @project)
#js-ci-pipeline-triggers-list.triggers-list{ data: { triggers: @triggers_json } }
- else
@@ -28,11 +28,11 @@
%th
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else
- %p.settings-message.text-center.gl-mb-3{ data: { testid: 'no_triggers_content' } }
- = _('No triggers exist yet. Use the form above to create one.')
-
- .card-footer
-
+ = render Pajamas::AlertComponent.new(variant: :warning, show_icon: false, dismissible: false,
+ alert_options: { data: { testid: 'no_triggers_content' }}) do |c|
+ = c.body do
+ = _('No triggers exist yet. Use the form above to create one.')
+ - c.footer do
%p
= _("These examples show how to trigger this project's pipeline for a branch or tag.")
diff --git a/app/views/projects/work_items/index.html.haml b/app/views/projects/work_items/index.html.haml
index 8575fd10ad3..69597aab7ef 100644
--- a/app/views/projects/work_items/index.html.haml
+++ b/app/views/projects/work_items/index.html.haml
@@ -1,5 +1,7 @@
- page_title s_('WorkItem|Work Items')
- add_page_specific_style 'page_bundles/work_items'
+- @gfm_form = true
+- @noteable_type = 'WorkItem'
#js-work-items{ data: work_items_index_data(@project) }
= render 'projects/invite_members_modal', project: @project
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
index 911ba5e8042..fe455f4a0bc 100644
--- a/app/views/registrations/welcome/show.html.haml
+++ b/app/views/registrations/welcome/show.html.haml
@@ -17,7 +17,11 @@
%p.gl-text-center= html_escape(_('%{gitlab_experience_text}. We won\'t share this information with anyone.')) % { gitlab_experience_text: gitlab_experience_text }
- else
%p.gl-text-center= html_escape(_('%{gitlab_experience_text}. Don\'t worry, this information isn\'t shared outside of your self-managed GitLab instance.')) % { gitlab_experience_text: gitlab_experience_text }
- = gitlab_ui_form_for(current_user, url: users_sign_up_welcome_path, html: { class: 'card gl-w-full! gl-p-5 js-users-signup-welcome', 'aria-live' => 'assertive' }) do |f|
+ = gitlab_ui_form_for(current_user,
+ url: users_sign_up_welcome_path(glm_tracking_params),
+ html: { class: 'card gl-w-full! gl-p-5 js-users-signup-welcome',
+ 'aria-live' => 'assertive',
+ data: { testid: 'welcome-form' } }) do |f|
.devise-errors
= render 'devise/shared/error_messages', resource: current_user
.row
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 168f4ca10bc..8262c3c90e1 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,26 +1,19 @@
- search_bar_classes = 'search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4'
+
= render_if_exists 'shared/promotions/promote_advanced_search'
-= render partial: 'search/results_status', locals: { search_service: @search_service } unless @search_objects.to_a.empty?
+- if Feature.enabled?(:search_page_vertical_nav, current_user) && %w[issues merge_requests].include?(@scope)
+ .results.gl-md-display-flex.gl-mt-0
+ #js-search-sidebar{ class: search_bar_classes, data: { navigation: search_navigation_json } }
+ .gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden
+ = render partial: 'search/results_status', locals: { search_service: @search_service } unless @search_objects.to_a.empty?
+ = render partial: 'search/results_list'
+
+- else
+ = render partial: 'search/results_status', locals: { search_service: @search_service } unless @search_objects.to_a.empty?
-.results.gl-md-display-flex.gl-mt-3
- - if %w[issues merge_requests].include?(@scope)
- #js-search-sidebar{ class: search_bar_classes }
- .gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden
- - if @timeout
- = render partial: "search/results/timeout"
- - elsif @search_objects.to_a.empty?
- = render partial: "search/results/empty"
- - else
- - if @scope == 'commits'
- %ul.content-list.commit-list
- = render partial: "search/results/commit", collection: @search_objects
- - else
- .search-results
- - if @scope == 'projects'
- .term
- = render 'shared/projects/list', projects: @search_objects, pipeline_status: false
- - else
- = render_if_exists partial: "search/results/#{@scope.singularize}", collection: @search_objects
+ .results.gl-md-display-flex.gl-mt-3
+ - if %w[issues merge_requests].include?(@scope)
+ #js-search-sidebar{ class: search_bar_classes, data: { navigation: search_navigation_json } }
- - if @scope != 'projects'
- = paginate_collection(@search_objects)
+ .gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden
+ = render partial: 'search/results_list'
diff --git a/app/views/search/_results_list.html.haml b/app/views/search/_results_list.html.haml
new file mode 100644
index 00000000000..cf910402ad4
--- /dev/null
+++ b/app/views/search/_results_list.html.haml
@@ -0,0 +1,18 @@
+- if @timeout
+ = render partial: "search/results/timeout"
+- elsif @search_objects.to_a.empty?
+ = render partial: "search/results/empty"
+- else
+ - if @scope == 'commits'
+ %ul.content-list.commit-list
+ = render partial: "search/results/commit", collection: @search_objects
+ - else
+ .search-results
+ - if @scope == 'projects'
+ .term
+ = render 'shared/projects/list', projects: @search_objects, pipeline_status: false
+ - else
+ = render_if_exists partial: "search/results/#{@scope.singularize}", collection: @search_objects
+
+ - if @scope != 'projects'
+ = paginate_collection(@search_objects)
diff --git a/app/views/search/_results_status.html.haml b/app/views/search/_results_status.html.haml
index ef5e3e83103..e6bb0c18b90 100644
--- a/app/views/search/_results_status.html.haml
+++ b/app/views/search/_results_status.html.haml
@@ -2,24 +2,8 @@
- return unless search_service.show_results_status?
-.search-results-status
- .row-content-block.gl-display-flex
- .gl-md-display-flex.gl-text-left.gl-align-items-center.gl-flex-grow-1
- - unless search_service.without_count?
- = search_entries_info(search_service.search_objects, search_service.scope, params[:search])
- - unless search_service.show_snippets?
- - if search_service.project
- - link_to_project = link_to(search_service.project.full_name, search_service.project, class: 'ml-md-1')
- - if search_service.scope == 'blobs'
- = _("in")
- .mx-md-1
- #js-blob-ref-switcher{ data: { "project-id" => search_service.project.id, "ref" => repository_ref(search_service.project), "field-name": "repository_ref" } }
- = s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project }
- - else
- = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
- - elsif search_service.group
- - link_to_group = link_to(search_service.group.name, search_service.group, class: 'ml-md-1')
- = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
- - if search_service.show_sort_dropdown?
- .gl-md-display-flex.gl-flex-direction-column
- #js-search-sort{ data: { "search-sort-options" => search_sort_options.to_json } }
+- if Feature.enabled?(:search_page_vertical_nav, current_user)
+ = render partial: 'search/results_status_vert_nav', locals: { search_service: @search_service }
+
+- else
+ = render partial: 'search/results_status_horiz_nav', locals: { search_service: @search_service }
diff --git a/app/views/search/_results_status_horiz_nav.html.haml b/app/views/search/_results_status_horiz_nav.html.haml
new file mode 100644
index 00000000000..fe6ee0f12ec
--- /dev/null
+++ b/app/views/search/_results_status_horiz_nav.html.haml
@@ -0,0 +1,22 @@
+.search-results-status
+ .row-content-block.gl-display-flex
+ .gl-md-display-flex.gl-text-left.gl-align-items-center.gl-flex-grow-1
+ - unless search_service.without_count?
+ = search_entries_info(search_service.search_objects, search_service.scope, params[:search])
+ - unless search_service.show_snippets?
+ - if search_service.project
+ - link_to_project = link_to(search_service.project.full_name, search_service.project, class: 'ml-md-1')
+ - if search_service.scope == 'blobs'
+ = _("in")
+ .mx-md-1
+ #js-blob-ref-switcher{ data: { "project-id" => search_service.project.id, "ref" => repository_ref(search_service.project), "field-name": "repository_ref" } }
+ = s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project }
+ - else
+ = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
+ - elsif search_service.group
+ - link_to_group = link_to(search_service.group.name, search_service.group, class: 'ml-md-1')
+ = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
+ - if search_service.show_sort_dropdown?
+ .gl-md-display-flex.gl-flex-direction-column
+ #js-search-sort{ data: { "search-sort-options" => search_sort_options.to_json } }
+
diff --git a/app/views/search/_results_status_vert_nav.html.haml b/app/views/search/_results_status_vert_nav.html.haml
new file mode 100644
index 00000000000..03916911f43
--- /dev/null
+++ b/app/views/search/_results_status_vert_nav.html.haml
@@ -0,0 +1,23 @@
+.search-results-status
+ .gl-display-flex.gl-flex-direction-column
+ .gl-p-5.gl-display-flex
+ .gl-md-display-flex.gl-text-left.gl-align-items-center.gl-flex-grow-1
+ - unless search_service.without_count?
+ = search_entries_info(search_service.search_objects, search_service.scope, params[:search])
+ - unless search_service.show_snippets?
+ - if search_service.project
+ - link_to_project = link_to(search_service.project.full_name, search_service.project, class: 'ml-md-1')
+ - if search_service.scope == 'blobs'
+ = _("in")
+ .mx-md-1
+ #js-blob-ref-switcher{ data: { "project-id" => search_service.project.id, "ref" => repository_ref(search_service.project), "field-name": "repository_ref" } }
+ = s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project }
+ - else
+ = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project }
+ - elsif search_service.group
+ - link_to_group = link_to(search_service.group.name, search_service.group, class: 'ml-md-1')
+ = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
+ - if search_service.show_sort_dropdown?
+ .gl-md-display-flex.gl-flex-direction-column
+ #js-search-sort{ data: { "search-sort-options" => search_sort_options.to_json } }
+ %hr.gl-mb-5.gl-mt-0.gl-border-gray-100.gl-w-full
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index a28d9effbdd..a811dabf399 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -15,6 +15,6 @@
%span
by
= link_to user_snippets_path(snippet_title.author) do
- = image_tag avatar_icon_for_user(snippet_title.author), class: "avatar avatar-inline s16", alt: ''
+ = render Pajamas::AvatarComponent.new(snippet_title.author, size: 16, class: 'gl-mt-n1')
= snippet_title.author_name
%span.light= time_ago_with_tooltip(snippet_title.created_at)
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index b59275c35df..d6900c397a0 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -4,6 +4,6 @@
%div{ class: 'search-result-row gl-pb-3! gl-mt-5 gl-mb-0!' }
%span.gl-display-flex.gl-align-items-center
= link_to wiki_blob_link, data: { track_action: 'click_text', track_label: "wiki_title", track_property: 'search_result' }, class: 'gl-w-full' do
- %span.term.str-truncated.gl-font-weight-bold= ::Gitlab::Git::Wiki::GollumSlug.canonicalize_filename(wiki_blob.path)
+ %span.term.str-truncated.gl-font-weight-bold= ::Wiki.canonicalize_filename(wiki_blob.path)
.description.term.col-sm-10.gl-px-0
= simple_search_highlight_and_truncate(wiki_blob.data, @search_term)
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index 5a45e512579..9d812e77ad4 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -22,5 +22,6 @@
.gl-mt-3
#js-search-topbar{ data: { "group-initial-data": group_attributes.to_json, "project-initial-data": project_attributes.to_json } }
- if @search_term
- = render 'search/category'
+ - if Feature.disabled?(:search_page_vertical_nav, current_user)
+ = render 'search/category'
= render 'search/results'
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 6b502ee928e..48ae1f7eb1d 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -9,14 +9,14 @@
%span.js-clone-dropdown-label
= default_clone_protocol.upcase
= sprite_icon('chevron-down', css_class: 'gl-icon')
- %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown
+ %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown{ data: { qa_selector: 'clone_dropdown_content' } }
%li
= ssh_clone_button(container)
%li
= http_clone_button(container)
= render_if_exists 'shared/kerberos_clone_button', container: container
- = text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus btn gl-button", readonly: true, aria: { label: _('Repository clone URL') }
+ = text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus btn gl-button", readonly: true, aria: { label: _('Repository clone URL') }, data: { qa_selector: 'clone_url_content' }
.input-group-append
= clipboard_button(target: '#clone_url', title: _("Copy URL"), class: "input-group-text gl-button btn-default btn-clipboard")
diff --git a/app/views/shared/_commit_well.html.haml b/app/views/shared/_commit_well.html.haml
deleted file mode 100644
index 48fe258d01f..00000000000
--- a/app/views/shared/_commit_well.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-.info-well.d-none.d-sm-block.project-last-commit.gl-mb-3
- .well-segment
- %ul.blob-commit-info
- = render 'projects/commits/commit', commit: commit, ref: ref, project: project
diff --git a/app/views/shared/_custom_attributes.html.haml b/app/views/shared/_custom_attributes.html.haml
index 966ab8e3cb1..6e5f1cb063c 100644
--- a/app/views/shared/_custom_attributes.html.haml
+++ b/app/views/shared/_custom_attributes.html.haml
@@ -1,12 +1,13 @@
- return unless custom_attributes.present?
-.card
- .card-header
+= render Pajamas::CardComponent.new(body_options: { class: 'gl-py-0' }) do |c|
+ - c.header do
= link_to(_('Custom Attributes'), help_page_path('api/custom_attributes.md'))
- %ul.content-list
- - custom_attributes.each do |custom_attribute|
- %li
- %span.light
- = custom_attribute.key
- %strong
- = custom_attribute.value
+ - c.body do
+ %ul.content-list
+ - custom_attributes.each do |custom_attribute|
+ %li
+ %span.light
+ = custom_attribute.key
+ %strong
+ = custom_attribute.value
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index 89be816fc76..73ace033dc6 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -4,11 +4,10 @@
- blame_path = project_blame_path(@project, tree_join(@ref, blob.path))
.line-numbers{ class: "gl-px-0!", data: { blame_path: blame_path } }
- if blob.data.present?
- - link = blob_link if defined?(blob_link)
- blob.data.each_line.each_with_index do |_, index|
- i = index + offset
-# We're not using `link_to` because it is too slow once we get to thousands of lines.
- %a.file-line-num.diff-line-num{ class: ("js-line-links" if Feature.enabled?(:file_line_blame)), href: "#{link}#L#{i}", id: "L#{i}", 'data-line-number' => i }
+ %a.file-line-num.diff-line-num{ class: ("js-line-links" if Feature.enabled?(:file_line_blame)), href: "#L#{i}", id: "L#{i}", 'data-line-number' => i }
= i
- highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil
.blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight, qa_selector: 'file_content' } }
diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml
index 0c88ac66b8b..eada58091b7 100644
--- a/app/views/shared/access_tokens/_form.html.haml
+++ b/app/views/shared/access_tokens/_form.html.haml
@@ -1,6 +1,7 @@
- ajax = local_assigns.fetch(:ajax, false)
- title = local_assigns.fetch(:title, _('Add a %{type}') % { type: type })
- prefix = local_assigns.fetch(:prefix, :personal_access_token)
+- description_prefix = local_assigns.fetch(:description_prefix, prefix)
- help_path = local_assigns.fetch(:help_path)
- resource = local_assigns.fetch(:resource, false)
- access_levels = local_assigns.fetch(:access_levels, false)
@@ -43,7 +44,7 @@
%p.text-secondary#select_scope_help_text
= s_('Tokens|Scopes set the permission levels granted to the token.')
= link_to _("Learn more."), help_path, target: '_blank', rel: 'noopener noreferrer'
- = render 'shared/tokens/scopes_form', prefix: prefix, token: token, scopes: scopes, f: f
+ = render 'shared/tokens/scopes_form', prefix: prefix, description_prefix: description_prefix, token: token, scopes: scopes, f: f
.gl-mt-3
- = f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-confirm', data: { qa_selector: 'create_token_button' }
+ = f.submit _('Create %{type}') % { type: type }, data: { qa_selector: 'create_token_button' }, pajamas_button: true
diff --git a/app/views/shared/blob/_markdown_buttons.html.haml b/app/views/shared/blob/_markdown_buttons.html.haml
index 4db1d20e81b..db53d78dadb 100644
--- a/app/views/shared/blob/_markdown_buttons.html.haml
+++ b/app/views/shared/blob/_markdown_buttons.html.haml
@@ -1,4 +1,5 @@
- modifier_key = client_js_flags[:isMac] ? '⌘' : s_('KeyboardKey|Ctrl+')
+- supports_file_upload = local_assigns.fetch(:supports_file_upload, true)
.md-header-toolbar.active
= markdown_toolbar_button({ icon: "bold",
@@ -23,14 +24,19 @@
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") })
= markdown_toolbar_button({ icon: "list-task", data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a checklist") })
+ = markdown_toolbar_button({ icon: "list-indent",
+ data: { "md-command" => 'indentLines', "md-shortcuts": '["mod+]"]' },
+ css_class: 'gl-display-none',
+ title: sprintf(s_("MarkdownEditor|Indent line (%{modifier_key}])") % { modifier_key: modifier_key }) })
+ = markdown_toolbar_button({ icon: "list-outdent",
+ data: { "md-command" => 'outdentLines', "md-shortcuts": '["mod+["]' },
+ css_class: 'gl-display-none',
+ title: sprintf(s_("MarkdownEditor|Outdent line (%{modifier_key}[)") % { modifier_key: modifier_key }) })
= markdown_toolbar_button({ icon: "details-block",
data: { "md-tag" => "<details><summary>Click to expand</summary>\n{text}\n</details>", "md-prepend" => true, "md-select" => "Click to expand" },
title: _("Add a collapsible section") })
- = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
- = markdown_toolbar_button({ icon: "paperclip",
- data: { "testid" => "button-attach-file" },
- css_class: 'js-attach-file-button markdown-selector',
- title: _("Attach a file or image") })
+ = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| | |\n| | |", "md-prepend" => true }, title: _("Add a table") })
+ - if supports_file_upload
+ = render Pajamas::ButtonComponent.new(icon: 'paperclip', category: :tertiary, button_options: { 'aria-label': _("Attach a file or image"), class: 'has-tooltip js-attach-file-button', data: { testid: 'button-attach-file', container: 'body' } })
- if show_fullscreen_button
- %button.gl-button.btn.btn-default-tertiary.btn-icon.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
- = sprite_icon("maximize")
+ = render Pajamas::ButtonComponent.new(icon: 'maximize', category: :tertiary, button_options: { 'tabindex': -1, 'aria-label': _("Go full screen"), class: 'has-tooltip js-zen-enter', data: { container: 'body' } })
diff --git a/app/views/shared/deploy_keys/_project_group_form.html.haml b/app/views/shared/deploy_keys/_project_group_form.html.haml
index d76ef8feb62..11fa44fe282 100644
--- a/app/views/shared/deploy_keys/_project_group_form.html.haml
+++ b/app/views/shared/deploy_keys/_project_group_form.html.haml
@@ -17,4 +17,4 @@
help_text: _('Allow this key to push to this repository')
.form-group.row
- = f.submit _("Add key"), class: "btn gl-button btn-confirm", data: { qa_selector: "add_deploy_key_button"}
+ = f.submit _("Add key"), data: { qa_selector: "add_deploy_key_button"}, pajamas_button: true
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index eade973d72a..1b48843eb10 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -34,4 +34,4 @@
= f.gitlab_ui_checkbox_component :write_package_registry, 'write_package_registry', help_text: s_('DeployTokens|Allows read and write access to the package registry.'), checkbox_options: { data: { qa_selector: 'deploy_token_write_package_registry_checkbox' } }
.gl-mt-3
- = f.submit s_('DeployTokens|Create deploy token'), class: 'btn gl-button btn-confirm', data: { qa_selector: 'create_deploy_token_button' }
+ = f.submit s_('DeployTokens|Create deploy token'), data: { qa_selector: 'create_deploy_token_button' }, pajamas_button: true
diff --git a/app/views/shared/deploy_tokens/_index.html.haml b/app/views/shared/deploy_tokens/_index.html.haml
index 79bf35e2726..faec379e42b 100644
--- a/app/views/shared/deploy_tokens/_index.html.haml
+++ b/app/views/shared/deploy_tokens/_index.html.haml
@@ -8,10 +8,20 @@
%p
= description
.settings-content
- - if @created_deploy_token
- = render 'shared/deploy_tokens/new_deploy_token', deploy_token: @created_deploy_token
- %h5.gl-mt-0
- = s_('DeployTokens|New deploy token')
- = render 'shared/deploy_tokens/form', group_or_project: group_or_project, token: @new_deploy_token, presenter: @deploy_tokens
+ - if Feature.enabled?(:ajax_new_deploy_token, group_or_project)
+ #js-new-deploy-token{ data: {
+ container_registry_enabled: container_registry_enabled?(group_or_project),
+ packages_registry_enabled: packages_registry_enabled?(group_or_project),
+ create_new_token_path: create_deploy_token_path(group_or_project),
+ token_type: group_or_project.is_a?(Group) ? 'group' : 'project',
+ deploy_tokens_help_url: help_page_path('user/project/deploy_tokens/index.md')
+ }
+ }
+ - else
+ - if @created_deploy_token
+ = render 'shared/deploy_tokens/new_deploy_token', deploy_token: @created_deploy_token
+ %h5.gl-mt-0
+ = s_('DeployTokens|New deploy token')
+ = render 'shared/deploy_tokens/form', group_or_project: group_or_project, token: @new_deploy_token, presenter: @deploy_tokens
%hr
= render 'shared/deploy_tokens/table', group_or_project: group_or_project, active_tokens: @deploy_tokens
diff --git a/app/views/shared/doorkeeper/applications/_index.html.haml b/app/views/shared/doorkeeper/applications/_index.html.haml
index b14ff9b2508..6a770a4fcb2 100644
--- a/app/views/shared/doorkeeper/applications/_index.html.haml
+++ b/app/views/shared/doorkeeper/applications/_index.html.haml
@@ -1,6 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
-.row.gl-mt-3
+.row.gl-mt-3.js-search-settings-section
.col-lg-4.profile-settings-sidebar
%h4.gl-mt-0
= page_title
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index 0bec94f70ea..e6bdefc64d2 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -14,7 +14,7 @@
.block
.title
= _('Status')
- .js-issue-status
+ .js-status-dropdown
.block
.title
= _('Assignee')
@@ -41,15 +41,7 @@
.block
.title
= _('Subscriptions')
- .filter-item
- = dropdown_tag(_("Select subscription"), options: { toggle_class: "js-subscription-event", title: _("Change subscription"), dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: _("Subscription") } } ) do
- %ul
- %li
- %a{ href: "#", data: { id: "subscribe" } }
- = _('Subscribe')
- %li
- %a{ href: "#", data: { id: "unsubscribe" } }
- = _('Unsubscribe')
+ .js-subscriptions-dropdown
= hidden_field_tag "update[issuable_ids]", []
= hidden_field_tag :state_event, params[:state_event]
diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml
index 69ff477d415..94b7fe14721 100644
--- a/app/views/shared/issuable/_feed_buttons.html.haml
+++ b/app/views/shared/issuable/_feed_buttons.html.haml
@@ -1,8 +1,8 @@
- show_calendar_button = local_assigns.fetch(:show_calendar_button, true)
-= link_to safe_params.merge(rss_url_options), class: 'btn gl-button btn-default btn-icon has-tooltip', data: { container: 'body', testid: 'rss-feed-link' }, title: _('Subscribe to RSS feed') , 'aria-label': _('Subscribe to RSS feed') do
- = sprite_icon('rss')
+= render Pajamas::ButtonComponent.new(href: safe_params.merge(rss_url_options), icon: 'rss', button_options: { class: 'has-tooltip', 'aria-label': _('Subscribe to RSS feed'), data: { container: 'body', testid: 'rss-feed-link' } }) do
+ = _('Subscribe to RSS feed')
- if show_calendar_button
- = link_to safe_params.merge(calendar_url_options), class: 'btn gl-button btn-default btn-icon has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar'), 'aria-label': _('Subscribe to calendar') do
- = sprite_icon('calendar')
+ = render Pajamas::ButtonComponent.new(href: safe_params.merge(calendar_url_options), icon: 'calendar', button_options: { class: 'has-tooltip', 'aria-label': _('Subscribe to calendar'), data: { container: 'body' } }) do
+ = _('Subscribe to calendar')
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 53eb6f4c63b..5b7f9c4226c 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -62,9 +62,9 @@
= sanitize(html_escape(_('Please review the %{linkStart}contribution guidelines%{linkEnd} for this project.')) % { linkStart: contribution_guidelines_start, linkEnd: contribution_guidelines_end })
- if issuable.new_record?
- = form.submit "#{_('Create')} #{issuable.class.model_name.human.downcase}", class: 'gl-button btn btn-confirm gl-mr-2', data: { qa_selector: 'issuable_create_button', track_experiment: 'promote_mr_approvals_in_free', track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
+ = form.submit "#{_('Create')} #{issuable.class.model_name.human.downcase}", class: 'gl-button btn btn-confirm gl-mr-2', data: { qa_selector: 'issuable_create_button', track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
- else
- = form.submit _('Save changes'), class: 'gl-button btn btn-confirm gl-mr-2', data: { track_experiment: 'promote_mr_approvals_in_free', track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
+ = form.submit _('Save changes'), class: 'gl-button btn btn-confirm gl-mr-2', data: { track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
- if issuable.new_record?
= link_to _('Cancel'), polymorphic_path([@project, issuable.class]), class: 'btn gl-button btn-default js-reset-autosave'
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index ec78b3f7ce3..eb3acd8e055 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -19,7 +19,7 @@
%input.js-add-list{ type: "checkbox", name: "add_list", checked: add_list }
%span= _('Add list')
.clearfix
- %button.gl-button.btn.btn-confirm.float-left.js-new-label-btn{ type: "button" }
+ = render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { class: 'float-left js-new-label-btn' }) do
= _('Create')
- %button.gl-button.btn.btn-default.float-right.js-cancel-label-btn{ type: "button" }
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'float-right js-cancel-label-btn' }) do
= _('Cancel')
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index ef539029272..58108ceeb76 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -11,10 +11,6 @@
placeholder: _('Search milestones'), footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: _('Milestone'), qa_selector: "issuable_milestone_dropdown", testid: "issuable-milestone-dropdown" } }) do
- if project
%ul.dropdown-footer-list
- - if can? current_user, :admin_milestone, project
- %li
- = link_to new_project_milestone_path(project), title: _('New Milestone') do
- = _('Create new')
%li
= link_to project_milestones_path(project) do
- if can? current_user, :admin_milestone, project
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 21716710015..72940b64801 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -11,10 +11,11 @@
- if params[:search].present?
= hidden_field_tag :search, params[:search]
- if @can_bulk_update
- .check-all-holder.gl-display-none.gl-sm-display-block.hidden.gl-float-left.gl-mr-5.gl-line-height-36
- - checkbox_id = 'check-all-issues'
- %label.gl-sr-only{ for: checkbox_id }= _('Select all')
- = check_box_tag checkbox_id, nil, false, class: "check-all-issues left"
+ .check-all-holder.gl-display-none.gl-sm-display-block.hidden.gl-float-left.gl-mr-3.gl-line-height-36
+ = render Pajamas::CheckboxTagComponent.new(name: 'check-all-issues', value: nil) do |c|
+ = c.label do
+ %span.gl-sr-only
+ = _('Select all')
.issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
.filtered-search-box
- if type != :boards
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index f2ce0676a9a..4199b7e870b 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -27,7 +27,7 @@
#js-severity
- if reviewers
- .block.reviewer
+ .block.reviewer{ data: { qa_selector: 'reviewers_block_container' } }
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
- if issuable_sidebar[:supports_escalation]
@@ -65,7 +65,7 @@
= gl_loading_icon(inline: true)
- if issuable_sidebar.dig(:features_available, :health_status)
- .js-sidebar-status-entry-point{ data: sidebar_status_data(issuable_sidebar, @project) }
+ .js-sidebar-health-status-entry-point{ data: sidebar_status_data(issuable_sidebar, @project) }
- if issuable_sidebar.has_key?(:confidential)
-# haml-lint:disable InlineJavaScript
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index e9b04579808..62221fb8218 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -39,7 +39,7 @@
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
+ - data['max-select'] = dropdown_max_select(dropdown_options[:data], :limit_assignees_per_issuable)
- options[:data].merge!(data)
= render 'shared/issuable/sidebar_user_dropdown',
diff --git a/app/views/shared/issuable/_sidebar_reviewers.html.haml b/app/views/shared/issuable/_sidebar_reviewers.html.haml
index 3f78f29ea24..771db8af6a8 100644
--- a/app/views/shared/issuable/_sidebar_reviewers.html.haml
+++ b/app/views/shared/issuable/_sidebar_reviewers.html.haml
@@ -36,7 +36,10 @@
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- - data['max-select'] = dropdown_max_select(dropdown_options[:data])
+ - data[:suggested_reviewers_header] = dropdown_options[:data][:suggested_reviewers_header]
+ - data[:all_members_header] = dropdown_options[:data][:all_members_header]
+ - data[:show_suggested] = dropdown_options[:data][:show_suggested]
+ - data['max-select'] = dropdown_max_select(dropdown_options[:data], :limit_reviewer_and_assignee_size)
- options[:data].merge!(data)
= render 'shared/issuable/sidebar_user_dropdown',
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index e36c4cd6be0..ccc1a9fda6e 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -1,5 +1,5 @@
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
-- viewing_merge_requests = controller.controller_name == 'merge_requests'
+- viewing_merge_requests = controller.controller_name == 'merge_requests' || controller.action_name == 'merge_requests'
- items = issuable_sort_options(viewing_issues, viewing_merge_requests)
- selected = issuable_sort_option_overrides[@sort] || @sort
diff --git a/app/views/shared/issue_type/_details_content.html.haml b/app/views/shared/issue_type/_details_content.html.haml
index 369aa53586f..8a9b71fd91e 100644
--- a/app/views/shared/issue_type/_details_content.html.haml
+++ b/app/views/shared/issue_type/_details_content.html.haml
@@ -2,7 +2,7 @@
- api_awards_path = local_assigns.fetch(:api_awards_path, nil)
.issue-details.issuable-details.js-issue-details
- .detail-page-description.content-block.js-detail-page-description
+ .detail-page-description.content-block.js-detail-page-description.gl-pb-0.gl-border-none
#js-issuable-app{ data: { initial: issuable_initial_data(issuable).to_json, issuable_id: issuable.id, full_path: @project.full_path } }
.title-container
%h1.title.page-title.gl-font-size-h-display= markdown_field(issuable, :title)
@@ -12,6 +12,9 @@
= edited_time_ago_with_tooltip(issuable, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
+ .js-issue-widgets
+ = render 'shared/issue_type/emoji_block', issuable: issuable, api_awards_path: api_awards_path
+
.js-issue-widgets
= render 'shared/issue_type/sentry_stack_trace', issuable: issuable
@@ -32,7 +35,6 @@
-# This element is filled in using JavaScript.
.js-issue-widgets
- = render 'shared/issue_type/emoji_block', issuable: issuable, api_awards_path: api_awards_path
= render 'projects/issues/discussion'
diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml
index 08fba712d5e..ccb501dae11 100644
--- a/app/views/shared/issue_type/_details_header.html.haml
+++ b/app/views/shared/issue_type/_details_header.html.haml
@@ -2,7 +2,7 @@
- badge_classes = 'issuable-status-badge gl-mr-3'
.detail-page-header
- .detail-page-header-body
+ .detail-page-header-body.gl-flex-wrap-wrap
= gl_badge_tag({ variant: :info, icon: 'issue-closed', icon_classes: 'gl-mr-0!' }, { class: "#{issue_status_visibility(issuable, status_box: :closed)} #{badge_classes} issuable-status-badge-closed" }) do
.gl-display-none.gl-sm-display-block.gl-ml-2
= issue_closed_text(issuable, current_user)
@@ -13,9 +13,8 @@
%span.gl-display-none.gl-sm-display-block.gl-ml-2
= _('Open')
- .issuable-meta
- #js-issuable-header-warnings{ data: { hidden: issue_hidden?(issuable).to_s } }
- = issuable_meta(issuable, @project)
+ #js-issuable-header-warnings{ data: { hidden: issue_hidden?(issuable).to_s } }
+ = issuable_meta(issuable, @project)
%a.btn.gl-button.btn-default.btn-icon.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left')
diff --git a/app/views/shared/issue_type/_emoji_block.html.haml b/app/views/shared/issue_type/_emoji_block.html.haml
index 61e28f18d3b..a5c71fb1d24 100644
--- a/app/views/shared/issue_type/_emoji_block.html.haml
+++ b/app/views/shared/issue_type/_emoji_block.html.haml
@@ -4,7 +4,5 @@
.row.gl-m-0.gl-justify-content-space-between
.js-noteable-awards
= render 'award_emoji/awards_block', awardable: issuable, inline: true, api_awards_path: api_awards_path
- .new-branch-col.gl-display-flex.gl-my-2.gl-font-size-0.gl-gap-3
- = render_if_exists "projects/issues/timeline_toggle", issuable: issuable
- #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(issuable), notes_filters: UserPreference.notes_filters.to_json } }
+ .new-branch-col.gl-font-size-0
= render 'new_branch' if show_new_branch_button?
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index e6d6d0998dc..c6932d49d33 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for @label, as: :label, url: url, html: { class: 'label-form js-quick-submit js-requires-input' } do |f|
+= gitlab_ui_form_for @label, as: :label, url: url, html: { class: 'label-form js-quick-submit js-requires-input' } do |f|
= form_errors(@label)
.form-group.row
@@ -26,9 +26,9 @@
.gl-display-flex.gl-justify-content-space-between
%div
- if @label.persisted?
- = f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2'
+ = f.submit _('Save changes'), class: 'js-save-button gl-mr-2', pajamas_button: true
- else
- = f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2', data: { qa_selector: 'label_create_button' }
+ = f.submit _('Create label'), class: 'js-save-button gl-mr-2', data: { qa_selector: 'label_create_button' }, pajamas_button: true
= link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel gl-mr-2'
- if @label.persisted?
- presented_label = @label.present
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 23f78f4be45..376e51a6b15 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -12,7 +12,7 @@
%li.member.js-member.py-2.px-3.d-flex.flex-column{ class: [dom_class(member), ("flex-md-row" unless force_mobile_view)], id: dom_id(member), data: { qa_selector: 'member_row' } }
%span.list-item-name.mb-2.m-md-0
- if user
- = image_tag avatar_icon_for_user(user, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: ''
+ = render Pajamas::AvatarComponent.new(user, size: 32, class: 'gl-mr-3 flex-shrink-0 flex-grow-0')
.user-info
%span.mr-1
= link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id }
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
index 98e2c6c43b1..31625c22a94 100644
--- a/app/views/shared/members/_requests.html.haml
+++ b/app/views/shared/members/_requests.html.haml
@@ -5,7 +5,7 @@
- return if requesters.empty?
-= render Pajamas::CardComponent.new(card_options: { class: 'gl-mt-3 gl-mb-5', data: { testid: 'access-requests' } }, body_options: { class: 'gl-p-0' }) do |c|
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5', data: { testid: 'access-requests' } }, body_options: { class: 'gl-p-0' }) do |c|
- c.header do
= _('Users requesting access to')
%strong= membership_source.name
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index 460ddd0897c..2502f7fca62 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -1,22 +1,20 @@
- show_counter = local_assigns.fetch(:show_counter, false)
- primary = local_assigns.fetch(:primary, false)
-- panel_class = primary ? 'bg-primary text-white' : ''
-.card
- .card-header{ class: panel_class }
- .header.gl-mb-2
- .title
- = title
- .issuable-count-weight.gl-ml-3
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }, body_options: { class: 'gl-py-0' }, header_options: { class: milestone_header_class(primary, issuables) }) do |c|
+ - c.header do
+ .gl-flex-grow-2
+ = title
+ .gl-ml-3.gl-flex-shrink-0.gl-font-weight-bold.gl-white-space-nowrap{ class: milestone_counter_class(primary) }
- if show_counter
- %span.counter
+ %span
= sprite_icon('issues', css_class: 'gl-vertical-align-text-bottom')
= number_with_delimiter(issuables.length)
= render_if_exists "shared/milestones/issuables_weight", issuables: issuables
-
- - class_prefix = dom_class(issuables).pluralize
- %ul{ class: "content-list milestone-#{class_prefix}-list", id: "#{class_prefix}-list-#{id}" }
- = render partial: 'shared/milestones/issuable',
- collection: issuables,
- as: :issuable,
- locals: { show_project_name: show_project_name }
+ = c.body do
+ - class_prefix = dom_class(issuables).pluralize
+ %ul{ class: "content-list milestone-#{class_prefix}-list", id: "#{class_prefix}-list-#{id}" }
+ = render partial: 'shared/milestones/issuable',
+ collection: issuables,
+ as: :issuable,
+ locals: { show_project_name: show_project_name }
diff --git a/app/views/shared/milestones/_participants_tab.html.haml b/app/views/shared/milestones/_participants_tab.html.haml
index fe83040c168..f90967c3b15 100644
--- a/app/views/shared/milestones/_participants_tab.html.haml
+++ b/app/views/shared/milestones/_participants_tab.html.haml
@@ -1,8 +1,8 @@
%ul.bordered-list
- users.each do |user|
%li
- = link_to user, title: user.name, class: "darken" do
- = image_tag avatar_icon_for_user(user, 32), class: "avatar s32"
- %strong= truncate(user.name, length: 40)
- %div
+ = link_to user, title: user.name, class: "gl-display-flex" do
+ = render Pajamas::AvatarComponent.new(user, size: 32, class: "gl-mr-3")
+ .gl-display-flex.gl-flex-direction-column
+ %strong= truncate(user.name, length: 40)
%small.cgray= user.username
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 3ab8514aebf..c552e94ac57 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -6,17 +6,18 @@
- note_counter = local_assigns.fetch(:note_counter, 0)
%li.timeline-entry.note-wrapper{ id: dom_id(note),
- class: ["note", "note-row-#{note.id}", ('system-note' if note.system)],
+ class: ["note", "note-comment", "note-row-#{note.id}", ('system-note' if note.system)],
data: { author_id: note.author.id,
editable: note_editable,
note_id: note.id } }
.timeline-entry-inner
- .timeline-icon
- - if note.system
+ - if note.system
+ .timeline-icon
= icon_for_system_note(note)
- - else
+ - else
+ .timeline-avatar.gl-float-left
%a.image-diff-avatar-link{ href: user_path(note.author) }
- = image_tag avatar_icon_for_user(note.author), alt: '', class: 'avatar s40'
+ = render Pajamas::AvatarComponent.new(note.author, size: 32, alt: '')
- if note.is_a?(DiffNote) && note.on_image?
- if show_image_comment_badge && note_counter == 0
-# Only show this for the first comment in the discussion
@@ -34,9 +35,9 @@
%span.note-header-author-name.bold
= note.author.name
= user_status(note.author)
- %span.note-headline-light{ data: { qa_selector: 'note_author_content' } }
+ %spannote-headline-light{ data: { qa_selector: 'note_author_content' } }
= note.author.to_reference
- %span.note-headline-light.note-headline-meta
+ %span.note-headline-ligh.note-headline-meta
- if note.system
%span.system-note-message
= markdown_field(note, :note)
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index ae264f2188f..81e2e066bd3 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -9,7 +9,7 @@
- compact_mode = false unless local_assigns[:compact_mode] == true
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
- css_class = '' unless local_assigns[:css_class]
-- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
+- css_class += " gl-display-flex!"
- cache_key = project_list_cache_key(project, pipeline_status: pipeline_status)
- updated_tooltip = time_ago_with_tooltip(project.last_activity_date)
- show_pipeline_status_icon = pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
@@ -18,15 +18,15 @@
- css_controls_class << "with-pipeline-status" if show_pipeline_status_icon && last_pipeline.present?
- avatar_container_class = project.creator && use_creator_avatar ? '' : 'rect-avatar'
-%li.project-row.d-flex{ class: css_class }
+%li.project-row.gl-align-items-center{ class: css_class }
= cache(cache_key) do
- if avatar
- .avatar-container.s48.flex-grow-0.flex-shrink-0{ class: avatar_container_class }
+ .flex-grow-0.flex-shrink-0{ class: avatar_container_class }
= link_to project_path(project), class: dom_class(project) do
- if project.creator && use_creator_avatar
- = image_tag avatar_icon_for_user(project.creator, 48), class: "avatar s48", alt: ''
+ = render Pajamas::AvatarComponent.new(project.creator, size: 48, alt: '', class: 'gl-mr-5')
- else
- = project_icon(project, alt: '', class: 'avatar project-avatar s48', width: 48, height: 48)
+ = render Pajamas::AvatarComponent.new(project, size: 48, alt: '', class: 'gl-mr-5')
.project-details.d-sm-flex.flex-sm-fill.align-items-center{ data: { qa_selector: 'project_content', qa_project_name: project.name } }
.flex-wrapper
.d-flex.align-items-center.flex-wrap.project-title
@@ -52,7 +52,7 @@
-# haml-lint:disable UnnecessaryStringOutput
= ' ' # prevent haml from eating the space between elements
.metadata-info.gl-mt-3
- %span.user-access-role.d-block{ data: { qa_selector: 'user_role_content' } }= Gitlab::Access.human_access(access)
+ %span.user-access-role.gl-display-block{ data: { qa_selector: 'user_role_content' } }= localized_project_human_access(access)
- if !explore_projects_tab?
.metadata-info.gl-mt-3
diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml
index a5170b199e8..e598343d698 100644
--- a/app/views/shared/projects/_search_form.html.haml
+++ b/app/views/shared/projects/_search_form.html.haml
@@ -1,4 +1,4 @@
-- form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : ''
+- form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : 'gl-w-full! gl-pl-7 '
- placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : _('Filter by name')
= form_tag filter_projects_path, method: :get, class: 'project-filter-form', data: { qa_selector: 'project_filter_form_container' }, id: 'project-filter-form' do |f|
diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml
index 436dbfd2b49..dc689303f77 100644
--- a/app/views/shared/runners/_runner_description.html.haml
+++ b/app/views/shared/runners/_runner_description.html.haml
@@ -1,6 +1,12 @@
.light.gl-mt-3
%p
- = _("Register as many runners as you want. You can register runners as separate users, on separate servers, and on your local machine. Runners are either:")
+ = s_("Runners|Register as many runners as you want. You can register runners as separate users, on separate servers, and on your local machine.")
+
+ %h5
+ = s_("Runners|How do runners pick up jobs?")
+
+ %p
+ = s_("Runners|Runners are either:")
%div
%ul
@@ -10,3 +16,7 @@
%li
= gl_badge_tag s_("Runners|paused"), variant: :danger, size: :sm
= _('- Not available to run jobs.')
+
+ %p
+ = s_("Runners|Tags control which type of jobs a runner can handle. By tagging a runner, you make sure shared runners only handle the jobs they are equipped to run.")
+ = link_to _("Learn more."), help_page_path("ci/runners/configure_runners", anchor: "use-tags-to-control-which-jobs-a-runner-can-run"), target: '_blank'
diff --git a/app/views/shared/runners/_shared_runners_description.html.haml b/app/views/shared/runners/_shared_runners_description.html.haml
index 2779901ca0c..c8ddb5d5176 100644
--- a/app/views/shared/runners/_shared_runners_description.html.haml
+++ b/app/views/shared/runners/_shared_runners_description.html.haml
@@ -1,12 +1,9 @@
--# "MaxBuilds" is a runner configuration keyword so it must not be translated.
-- link = link_to 'MaxBuilds', 'https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runnersmachine-section', target: '_blank', rel: 'noopener noreferrer'
+- shared_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('ci/runners/runners_scope.md', anchor: 'shared-runners') }
%h4
= _('Shared runners')
.bs-callout{ data: { testid: 'shared-runners-description' } }
- %p= _('These runners are shared across this GitLab instance.')
+ %p= s_('Runners|%{link_start}These runners%{link_end} are available to all groups and projects.').html_safe % { link_start: shared_link_start, link_end: '</a>'.html_safe }
- if Gitlab::CurrentSettings.shared_runners_text.present?
= markdown(Gitlab::CurrentSettings.current_application_settings.shared_runners_text)
- - else
- %p= _('The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com).').html_safe % { link: link }
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 3cd70dab4d5..6caadeb0ba4 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -2,7 +2,7 @@
- notes_count = @noteable_meta_data[snippet.id].user_notes_count
%li.snippet-row.py-3{ data: { qa_selector: 'snippet_link', qa_snippet_title: snippet.title } }
- = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
+ = render Pajamas::AvatarComponent.new(snippet.author, size: 48, alt: "", class: 'gl-display-none gl-sm-display-block gl-float-left gl-mr-3')
= link_to gitlab_snippet_path(snippet), class: "title" do
= snippet.title
@@ -20,16 +20,15 @@
= visibility_level_icon(snippet.visibility_level)
.snippet-info
- #{snippet.to_reference} &middot;
- authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')}
- by
- = link_to user_snippets_path(snippet.author), class: "js-user-link", data: { user_id: snippet.author.id } do
- = snippet.author_name
- - if link_project && snippet.project_id?
- %span.d-none.d-sm-inline-block
- in
- = link_to project_path(snippet.project) do
- = snippet.project.full_name
+ .gl-display-inline{ data: { testid: 'snippet-created-at'} }
+ - created_at = time_ago_with_tooltip(snippet.created_at, placement: 'bottom')
+ - author = link_to(snippet.author_name, user_snippets_path(snippet.author), data: { user_id: snippet.author.id })
+ #{snippet.to_reference} &middot;
+ - if link_project && snippet.project_id?
+ - project_link = link_to(snippet.project.full_name, project_path(snippet.project))
+ = _('created %{timeAgo} by %{author} in %{project_link}').html_safe % { timeAgo: created_at, author: author, project_link: project_link }
+ - else
+ = _('created %{timeAgo} by %{author}').html_safe % { timeAgo: created_at, author: author }
- .float-right.snippet-updated-at
- %span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
+ .float-right
+ = _('updated %{timeAgo}').html_safe % { timeAgo: time_ago_with_tooltip(snippet.updated_at, placement: 'bottom') }
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index 010376464f1..1c63ce490ed 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -1,11 +1,12 @@
- scopes = local_assigns.fetch(:scopes)
- prefix = local_assigns.fetch(:prefix)
+- description_prefix = local_assigns.fetch(:description_prefix, prefix)
- token = local_assigns.fetch(:token)
- f = local_assigns.fetch(:f)
%fieldset
- scopes.each do |scope|
- - help_text = t scope, scope: scope_description(prefix)
+ - help_text = t scope, scope: scope_description(description_prefix)
= f.gitlab_ui_checkbox_component :scopes, scope,
help_text: help_text,
checkbox_options: { checked: token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", multiple: true, data: { qa_selector: "#{scope}_checkbox" } },
diff --git a/app/views/shared/users/_user.html.haml b/app/views/shared/users/_user.html.haml
index 93b3ce5f319..51eb24f6d4a 100644
--- a/app/views/shared/users/_user.html.haml
+++ b/app/views/shared/users/_user.html.haml
@@ -3,7 +3,7 @@
.col-lg-3.col-md-4.col-sm-12
= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }) do |c|
= c.body do
- = image_tag avatar_icon_for_user(user, 40), class: "avatar s40", alt: ''
+ = render Pajamas::AvatarComponent.new(user, size: 48, alt: "", class: 'gl-float-left gl-mr-3')
.user-info
.block-truncated
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index afe72767b9a..c95e63bdc83 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -1,10 +1,13 @@
= form_errors(hook)
-.form-group
- = form.label :url, s_('Webhooks|URL'), class: 'label-bold'
- = form.text_field :url, class: 'form-control gl-form-input', placeholder: 'http://example.com/trigger-ci.json'
- %p.form-text.text-muted
- = s_('Webhooks|URL must be percent-encoded if it contains one or more special characters.')
+- if Feature.enabled?(:webhook_form_mask_url)
+ .js-vue-webhook-form{ data: webhook_form_data(hook) }
+- else
+ .form-group
+ = form.label :url, s_('Webhooks|URL'), class: 'label-bold'
+ = form.text_field :url, class: 'form-control gl-form-input', placeholder: 'http://example.com/trigger-ci.json'
+ %p.form-text.text-muted
+ = s_('Webhooks|URL must be percent-encoded if it contains one or more special characters.')
.form-group
= form.label :token, s_('Webhooks|Secret token'), class: 'label-bold'
= form.text_field :token, class: 'form-control gl-form-input', placeholder: ''
diff --git a/app/views/shared/web_hooks/_hook_errors.html.haml b/app/views/shared/web_hooks/_hook_errors.html.haml
index d95efe83e15..098cc19c435 100644
--- a/app/views/shared/web_hooks/_hook_errors.html.haml
+++ b/app/views/shared/web_hooks/_hook_errors.html.haml
@@ -4,16 +4,12 @@
- link_end = '</a>'.html_safe
- if hook.rate_limited?
- - support_path = 'https://support.gitlab.com/hc/en-us/requests/new'
- - placeholders = { strong_start: strong_start,
- strong_end: strong_end,
- limit: hook.rate_limit,
- support_link_start: link_start % { url: support_path },
- support_link_end: link_end }
- = render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook was automatically disabled'),
+ - placeholders = { limit: number_with_delimiter(hook.rate_limit),
+ root_namespace: hook.parent.root_namespace.path }
+ = render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook rate limit has been reached'),
variant: :danger) do |c|
= c.body do
- = s_('Webhooks|The webhook was triggered more than %{limit} times per minute and is now disabled. To re-enable this webhook, fix the problems shown in %{strong_start}Recent events%{strong_end}, then re-test your settings. %{support_link_start}Contact Support%{support_link_end} if you need help re-enabling your webhook.').html_safe % placeholders
+ = s_("Webhooks|Webhooks for %{root_namespace} are now disabled because they've been triggered more than %{limit} times per minute. They'll be automatically re-enabled in the next minute.").html_safe % placeholders
- elsif hook.permanently_disabled?
= render Pajamas::AlertComponent.new(title: s_('Webhooks|Webhook failed to connect'),
variant: :danger) do |c|
diff --git a/app/views/shared/web_hooks/_index.html.haml b/app/views/shared/web_hooks/_index.html.haml
index 5ec82ad6702..868633143cd 100644
--- a/app/views/shared/web_hooks/_index.html.haml
+++ b/app/views/shared/web_hooks/_index.html.haml
@@ -1,14 +1,13 @@
%hr
-.card#webhooks-index
- .card-header
- %h5
- = hook_class.underscore.humanize.titleize.pluralize
- (#{hooks.size})
-
- - if hooks.any?
- %ul.content-list
- - hooks.each do |hook|
- = render 'shared/web_hooks/hook', hook: hook
- - else
- %p.text-center.gl-mt-3.gl-mb-3
- = _('No webhooks enabled. Select trigger events above.')
+= render Pajamas::CardComponent.new(card_options: { id: 'webhooks-index' }, body_options: { class: 'gl-py-0'}) do |c|
+ - c.header do
+ = hook_class.underscore.humanize.titleize.pluralize
+ (#{hooks.size})
+ - c.body do
+ - if hooks.any?
+ %ul.content-list
+ - hooks.each do |hook|
+ = render 'shared/web_hooks/hook', hook: hook
+ - else
+ %p.text-center.gl-mt-3.gl-mb-3
+ = _('No webhooks enabled. Select trigger events above.')
diff --git a/app/views/shared/wikis/pages.html.haml b/app/views/shared/wikis/pages.html.haml
index 21d63a6db3d..e1252e91c10 100644
--- a/app/views/shared/wikis/pages.html.haml
+++ b/app/views/shared/wikis/pages.html.haml
@@ -2,7 +2,6 @@
- breadcrumb_title s_("Wiki|Pages")
- page_title s_("Wiki|Pages"), _("Wiki")
- add_page_specific_style 'page_bundles/wiki'
-- wiki_sort_options = [{ text: s_("Wiki|Title"), value: 'title', href: wiki_path(@wiki, action: :pages, sort: Wiki::TITLE_ORDER)}, { text: s_("Wiki|Created date"), value: 'created_at', href: wiki_path(@wiki, action: :pages, sort: Wiki::CREATED_AT_ORDER) }]
.wiki-page-header.top-area.flex-column.flex-lg-row
%h1.page-title.gl-font-size-h-display.gl-flex-grow-1
@@ -15,8 +14,7 @@
.dropdown.inline.wiki-sort-dropdown
.btn-group{ role: 'group' }
- = gl_redirect_listbox_tag wiki_sort_options, params[:sort], data: { right: true }
- = wiki_sort_controls(@wiki, params[:sort], params[:direction])
+ = wiki_sort_controls(@wiki, params[:direction])
%ul.wiki-pages-list.content-list
= render @wiki_entries, context: 'pages'
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index f1093a3b730..47ccc449e1b 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -14,7 +14,7 @@
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
-.row-content-block.top-block.content-component-block
+.row-content-block.top-block.content-component-block.gl-px-0.gl-py-2
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 9b282340d0a..a0f6da57f9e 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1020,6 +1020,42 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: github_importer:github_import_attachments_import_issue
+ :worker_name: Gitlab::GithubImport::Attachments::ImportIssueWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: github_importer:github_import_attachments_import_merge_request
+ :worker_name: Gitlab::GithubImport::Attachments::ImportMergeRequestWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: github_importer:github_import_attachments_import_note
+ :worker_name: Gitlab::GithubImport::Attachments::ImportNoteWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: github_importer:github_import_attachments_import_release
+ :worker_name: Gitlab::GithubImport::Attachments::ImportReleaseWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
- :name: github_importer:github_import_import_diff_note
:worker_name: Gitlab::GithubImport::ImportDiffNoteWorker
:feature_category: :importers
@@ -2181,6 +2217,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: ci_parse_secure_file_metadata
+ :worker_name: Ci::ParseSecureFileMetadataWorker
+ :feature_category: :mobile_signing_deployment
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: ci_runners_process_runner_version_update
:worker_name: Ci::Runners::ProcessRunnerVersionUpdateWorker
:feature_category: :runner_fleet
@@ -2352,15 +2397,6 @@
:weight: 1
:idempotent: false
:tags: []
-- :name: experiments_record_conversion_event
- :worker_name: Experiments::RecordConversionEventWorker
- :feature_category: :users
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
- :name: export_csv
:worker_name: ExportCsvWorker
:feature_category: :team_planning
@@ -2739,42 +2775,6 @@
:weight: 1
:idempotent: false
:tags: []
-- :name: namespaces_onboarding_issue_created
- :worker_name: Namespaces::OnboardingIssueCreatedWorker
- :feature_category: :onboarding
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
-- :name: namespaces_onboarding_pipeline_created
- :worker_name: Namespaces::OnboardingPipelineCreatedWorker
- :feature_category: :onboarding
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
-- :name: namespaces_onboarding_progress
- :worker_name: Namespaces::OnboardingProgressWorker
- :feature_category: :onboarding
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :cpu
- :weight: 1
- :idempotent: true
- :tags: []
-- :name: namespaces_onboarding_user_added
- :worker_name: Namespaces::OnboardingUserAddedWorker
- :feature_category: :onboarding
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
- :name: namespaces_process_sync_events
:worker_name: Namespaces::ProcessSyncEventsWorker
:feature_category: :pods
@@ -2820,6 +2820,42 @@
:weight: 2
:idempotent: false
:tags: []
+- :name: onboarding_issue_created
+ :worker_name: Onboarding::IssueCreatedWorker
+ :feature_category: :onboarding
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
+- :name: onboarding_pipeline_created
+ :worker_name: Onboarding::PipelineCreatedWorker
+ :feature_category: :onboarding
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
+- :name: onboarding_progress
+ :worker_name: Onboarding::ProgressWorker
+ :feature_category: :onboarding
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :cpu
+ :weight: 1
+ :idempotent: true
+ :tags: []
+- :name: onboarding_user_added
+ :worker_name: Onboarding::UserAddedWorker
+ :feature_category: :onboarding
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: packages_composer_cache_update
:worker_name: Packages::Composer::CacheUpdateWorker
:feature_category: :package_registry
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index c7efc92b25e..d5eca86744e 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -22,10 +22,9 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
created_entities.find_each do |entity|
BulkImports::CreatePipelineTrackersService.new(entity).execute!
- BulkImports::ExportRequestWorker.perform_async(entity.id)
- BulkImports::EntityWorker.perform_async(entity.id)
-
entity.start!
+
+ BulkImports::ExportRequestWorker.perform_async(entity.id)
end
re_enqueue
diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb
index f6b1c693fe4..ada3210624c 100644
--- a/app/workers/bulk_imports/entity_worker.rb
+++ b/app/workers/bulk_imports/entity_worker.rb
@@ -15,9 +15,11 @@ module BulkImports
if stage_running?(entity_id, current_stage)
logger.info(
structured_payload(
- entity_id: entity_id,
+ bulk_import_entity_id: entity_id,
+ bulk_import_id: bulk_import_id(entity_id),
current_stage: current_stage,
- message: 'Stage running'
+ message: 'Stage running',
+ importer: 'gitlab_migration'
)
)
@@ -26,9 +28,11 @@ module BulkImports
logger.info(
structured_payload(
- entity_id: entity_id,
+ bulk_import_entity_id: entity_id,
+ bulk_import_id: bulk_import_id(entity_id),
current_stage: current_stage,
- message: 'Stage starting'
+ message: 'Stage starting',
+ importer: 'gitlab_migration'
)
)
@@ -42,13 +46,17 @@ module BulkImports
rescue StandardError => e
logger.error(
structured_payload(
- entity_id: entity_id,
+ bulk_import_entity_id: entity_id,
+ bulk_import_id: bulk_import_id(entity_id),
current_stage: current_stage,
- message: e.message
+ message: e.message,
+ importer: 'gitlab_migration'
)
)
- Gitlab::ErrorTracking.track_exception(e, entity_id: entity_id)
+ Gitlab::ErrorTracking.track_exception(
+ e, bulk_import_entity_id: entity_id, bulk_import_id: bulk_import_id(entity_id), importer: 'gitlab_migration'
+ )
end
private
@@ -63,6 +71,10 @@ module BulkImports
BulkImports::Tracker.next_pipeline_trackers_for(entity_id).update(status_event: 'enqueue')
end
+ def bulk_import_id(entity_id)
+ @bulk_import_id ||= Entity.find(entity_id).bulk_import_id
+ end
+
def logger
@logger ||= Gitlab::Import::Logger.build
end
diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb
index 0d3e4f013dd..a57071ddcf1 100644
--- a/app/workers/bulk_imports/export_request_worker.rb
+++ b/app/workers/bulk_imports/export_request_worker.rb
@@ -13,45 +13,112 @@ module BulkImports
def perform(entity_id)
entity = BulkImports::Entity.find(entity_id)
+ entity.update!(source_xid: entity_source_xid(entity)) if entity.source_xid.nil?
+
request_export(entity)
+
+ BulkImports::EntityWorker.perform_async(entity_id)
rescue BulkImports::NetworkError => e
- log_export_failure(e, entity)
+ if e.retriable?(entity)
+ retry_request(e, entity)
+ else
+ log_export_failure(e, entity)
- entity.fail_op!
+ entity.fail_op!
+ end
end
private
def request_export(entity)
- http_client(entity.bulk_import.configuration).post(entity.export_relations_url_path)
+ http_client(entity).post(entity.export_relations_url_path)
end
- def http_client(configuration)
+ def http_client(entity)
@client ||= Clients::HTTP.new(
- url: configuration.url,
- token: configuration.access_token
+ url: entity.bulk_import.configuration.url,
+ token: entity.bulk_import.configuration.access_token
)
end
def log_export_failure(exception, entity)
- attributes = {
+ Gitlab::Import::Logger.error(
+ structured_payload(
+ log_attributes(exception, entity).merge(
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ message: "Request to export #{entity.source_type} failed",
+ importer: 'gitlab_migration'
+ )
+ )
+ )
+
+ BulkImports::Failure.create(log_attributes(exception, entity))
+ end
+
+ def log_attributes(exception, entity)
+ {
bulk_import_entity_id: entity.id,
pipeline_class: 'ExportRequestWorker',
exception_class: exception.class.to_s,
exception_message: exception.message.truncate(255),
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
}
+ end
+
+ def graphql_client(entity)
+ @graphql_client ||= BulkImports::Clients::Graphql.new(
+ url: entity.bulk_import.configuration.url,
+ token: entity.bulk_import.configuration.access_token
+ )
+ end
+
+ def entity_source_xid(entity)
+ query = entity_query(entity)
+ client = graphql_client(entity)
+
+ response = client.execute(
+ client.parse(query.to_s),
+ { full_path: entity.source_full_path }
+ ).original_hash
+
+ ::GlobalID.parse(response.dig(*query.data_path, 'id')).model_id
+ rescue StandardError => e
+ Gitlab::Import::Logger.error(
+ structured_payload(
+ log_attributes(e, entity).merge(
+ message: 'Failed to fetch source entity id',
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ importer: 'gitlab_migration'
+ )
+ )
+ )
+
+ nil
+ end
+
+ def entity_query(entity)
+ if entity.group?
+ BulkImports::Groups::Graphql::GetGroupQuery.new(context: nil)
+ else
+ BulkImports::Projects::Graphql::GetProjectQuery.new(context: nil)
+ end
+ end
+ def retry_request(exception, entity)
Gitlab::Import::Logger.error(
structured_payload(
- attributes.merge(
- bulk_import_id: entity.bulk_import.id,
- bulk_import_entity_type: entity.source_type
+ log_attributes(exception, entity).merge(
+ message: 'Retrying export request',
+ bulk_import_id: entity.bulk_import_id,
+ bulk_import_entity_type: entity.source_type,
+ importer: 'gitlab_migration'
)
)
)
- BulkImports::Failure.create(attributes)
+ self.class.perform_in(2.seconds, entity.id)
end
end
end
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index e171ec1e194..6d314774cff 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -19,8 +19,11 @@ module BulkImports
if pipeline_tracker.present?
logger.info(
structured_payload(
- entity_id: pipeline_tracker.entity.id,
- pipeline_name: pipeline_tracker.pipeline_name
+ bulk_import_entity_id: pipeline_tracker.entity.id,
+ bulk_import_id: pipeline_tracker.entity.bulk_import_id,
+ pipeline_name: pipeline_tracker.pipeline_name,
+ message: 'Pipeline starting',
+ importer: 'gitlab_migration'
)
)
@@ -28,9 +31,11 @@ module BulkImports
else
logger.error(
structured_payload(
- entity_id: entity_id,
+ bulk_import_entity_id: entity_id,
+ bulk_import_id: bulk_import_id(entity_id),
pipeline_tracker_id: pipeline_tracker_id,
- message: 'Unstarted pipeline not found'
+ message: 'Unstarted pipeline not found',
+ importer: 'gitlab_migration'
)
)
end
@@ -44,9 +49,10 @@ module BulkImports
attr_reader :pipeline_tracker
def run
- raise(Entity::FailedError, 'Failed entity status') if pipeline_tracker.entity.failed?
+ return skip_tracker if pipeline_tracker.entity.failed?
+
raise(Pipeline::ExpiredError, 'Pipeline timeout') if job_timeout?
- raise(Pipeline::FailedError, export_status.error) if export_failed?
+ raise(Pipeline::FailedError, "Export from source instance failed: #{export_status.error}") if export_failed?
return re_enqueue if export_empty? || export_started?
@@ -59,21 +65,29 @@ module BulkImports
fail_tracker(e)
end
+ def bulk_import_id(entity_id)
+ @bulk_import_id ||= Entity.find(entity_id).bulk_import_id
+ end
+
def fail_tracker(exception)
pipeline_tracker.update!(status_event: 'fail_op', jid: jid)
logger.error(
structured_payload(
- entity_id: pipeline_tracker.entity.id,
+ bulk_import_entity_id: pipeline_tracker.entity.id,
+ bulk_import_id: pipeline_tracker.entity.bulk_import_id,
pipeline_name: pipeline_tracker.pipeline_name,
- message: exception.message
+ message: exception.message,
+ importer: 'gitlab_migration'
)
)
Gitlab::ErrorTracking.track_exception(
exception,
- entity_id: pipeline_tracker.entity.id,
- pipeline_name: pipeline_tracker.pipeline_name
+ bulk_import_entity_id: pipeline_tracker.entity.id,
+ bulk_import_id: pipeline_tracker.entity.bulk_import_id,
+ pipeline_name: pipeline_tracker.pipeline_name,
+ importer: 'gitlab_migration'
)
BulkImports::Failure.create(
@@ -138,9 +152,11 @@ module BulkImports
def retry_tracker(exception)
logger.error(
structured_payload(
- entity_id: pipeline_tracker.entity.id,
+ bulk_import_entity_id: pipeline_tracker.entity.id,
+ bulk_import_id: pipeline_tracker.entity.bulk_import_id,
pipeline_name: pipeline_tracker.pipeline_name,
- message: "Retrying error: #{exception.message}"
+ message: "Retrying error: #{exception.message}",
+ importer: 'gitlab_migration'
)
)
@@ -148,5 +164,19 @@ module BulkImports
re_enqueue(exception.retry_delay)
end
+
+ def skip_tracker
+ logger.info(
+ structured_payload(
+ bulk_import_entity_id: pipeline_tracker.entity.id,
+ bulk_import_id: pipeline_tracker.entity.bulk_import_id,
+ pipeline_name: pipeline_tracker.pipeline_name,
+ message: 'Skipping pipeline due to failed entity',
+ importer: 'gitlab_migration'
+ )
+ )
+
+ pipeline_tracker.update!(status_event: 'skip', jid: jid)
+ end
end
end
diff --git a/app/workers/ci/cancel_pipeline_worker.rb b/app/workers/ci/cancel_pipeline_worker.rb
index 147839a0625..2735498b6bb 100644
--- a/app/workers/ci/cancel_pipeline_worker.rb
+++ b/app/workers/ci/cancel_pipeline_worker.rb
@@ -10,6 +10,7 @@ module Ci
idempotent!
deduplicate :until_executed
urgency :high
+ loggable_arguments 1
def perform(pipeline_id, auto_canceled_by_pipeline_id)
::Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
diff --git a/app/workers/ci/parse_secure_file_metadata_worker.rb b/app/workers/ci/parse_secure_file_metadata_worker.rb
new file mode 100644
index 00000000000..0d2495d3155
--- /dev/null
+++ b/app/workers/ci/parse_secure_file_metadata_worker.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Ci
+ class ParseSecureFileMetadataWorker
+ include ::ApplicationWorker
+
+ feature_category :mobile_signing_deployment
+ urgency :low
+ idempotent!
+
+ def perform(secure_file_id)
+ ::Ci::SecureFile.find_by_id(secure_file_id).try(&:update_metadata!)
+ end
+ end
+end
diff --git a/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb b/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
index 590514424bb..2a1f492cacb 100644
--- a/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
+++ b/app/workers/ci/pipeline_success_unlock_artifacts_worker.rb
@@ -13,7 +13,9 @@ module Ci
def perform(pipeline_id)
::Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
- break unless pipeline.has_archive_artifacts?
+ # TODO: Move this check inside the Ci::UnlockArtifactsService
+ # once the feature flags in it have been removed.
+ break unless pipeline.has_erasable_artifacts?
results = ::Ci::UnlockArtifactsService
.new(pipeline.project, pipeline.user)
diff --git a/app/workers/clusters/applications/uninstall_worker.rb b/app/workers/clusters/applications/uninstall_worker.rb
index da290eaf1f6..b71f87014aa 100644
--- a/app/workers/clusters/applications/uninstall_worker.rb
+++ b/app/workers/clusters/applications/uninstall_worker.rb
@@ -14,11 +14,7 @@ module Clusters
worker_has_external_dependencies!
loggable_arguments 0
- def perform(app_name, app_id)
- find_application(app_name, app_id) do |app|
- Clusters::Applications::UninstallService.new(app).execute
- end
- end
+ def perform(app_name, app_id); end
end
end
end
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
index c1fec4f0196..f51c2852da6 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -2,7 +2,7 @@
require 'sidekiq/api'
-Sidekiq::Worker.extend ActiveSupport::Concern
+Sidekiq::Worker.extend ActiveSupport::Concern # rubocop:disable Cop/SidekiqApiUsage
module ApplicationWorker
extend ActiveSupport::Concern
@@ -134,10 +134,6 @@ module ApplicationWorker
@log_bulk_perform_async = true
end
- def queue_size
- Sidekiq::Queue.new(queue).size
- end
-
def bulk_perform_async(args_list)
if log_bulk_perform_async?
Sidekiq.logger.info('class' => self.name, 'args_list' => args_list, 'args_list_count' => args_list.length, 'message' => 'Inserting multiple jobs')
@@ -177,7 +173,7 @@ module ApplicationWorker
end
in_safe_limit_batches(args_list, schedule_at) do |args_batch, schedule_at_for_batch|
- Sidekiq::Client.push_bulk('class' => self, 'args' => args_batch, 'at' => schedule_at_for_batch)
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_batch, 'at' => schedule_at_for_batch) # rubocop:disable Cop/SidekiqApiUsage
end
end
@@ -185,7 +181,7 @@ module ApplicationWorker
def do_push_bulk(args_list)
in_safe_limit_batches(args_list) do |args_batch, _|
- Sidekiq::Client.push_bulk('class' => self, 'args' => args_batch)
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_batch) # rubocop:disable Cop/SidekiqApiUsage
end
end
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index c2cd50d8c21..9793278ac0c 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -37,27 +37,22 @@ module Gitlab
importer_class.new(object, project, client).execute
- Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :imported)
+ if increment_object_counter?(object)
+ Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :imported)
+ end
info(project.id, message: 'importer finished')
rescue NoMethodError => e
# This exception will be more useful in development when a new
# Representation is created but the developer forgot to add a
# `:github_identifiers` field.
- Gitlab::Import::ImportFailureService.track(
- project_id: project.id,
- error_source: importer_class.name,
- exception: e,
- fail_import: true
- )
-
- raise(e)
+ track_and_raise_exception(project, e, fail_import: true)
rescue StandardError => e
- Gitlab::Import::ImportFailureService.track(
- project_id: project.id,
- error_source: importer_class.name,
- exception: e
- )
+ track_and_raise_exception(project, e)
+ end
+
+ def increment_object_counter?(_object)
+ true
end
def object_type
@@ -90,6 +85,17 @@ module Gitlab
github_identifiers: github_identifiers
)
end
+
+ def track_and_raise_exception(project, exception, fail_import: false)
+ Gitlab::Import::ImportFailureService.track(
+ project_id: project.id,
+ error_source: importer_class.name,
+ exception: exception,
+ fail_import: fail_import
+ )
+
+ raise(exception)
+ end
end
end
end
diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb
index b12c2311ea8..1feaaf917b2 100644
--- a/app/workers/concerns/gitlab/github_import/stage_methods.rb
+++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb
@@ -63,6 +63,10 @@ module Gitlab
import_stage: self.class.name
)
end
+
+ def import_settings(project)
+ Gitlab::GithubImport::Settings.new(project)
+ end
end
end
end
diff --git a/app/workers/experiments/record_conversion_event_worker.rb b/app/workers/experiments/record_conversion_event_worker.rb
deleted file mode 100644
index 6487f030628..00000000000
--- a/app/workers/experiments/record_conversion_event_worker.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Experiments
- class RecordConversionEventWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
-
- feature_category :users
- urgency :low
-
- idempotent!
-
- def perform(experiment, user_id)
- return unless Gitlab::Experimentation.active?(experiment)
-
- ::Experiment.record_conversion_event(experiment, user_id)
- end
- end
-end
diff --git a/app/workers/gitlab/github_import/attachments/import_issue_worker.rb b/app/workers/gitlab/github_import/attachments/import_issue_worker.rb
new file mode 100644
index 00000000000..1a9fa15b850
--- /dev/null
+++ b/app/workers/gitlab/github_import/attachments/import_issue_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Attachments
+ class ImportIssueWorker # rubocop:disable Scalability/IdempotentWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::NoteText
+ end
+
+ def importer_class
+ Importer::NoteAttachmentsImporter
+ end
+
+ def object_type
+ :issue_attachment
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/attachments/import_merge_request_worker.rb b/app/workers/gitlab/github_import/attachments/import_merge_request_worker.rb
new file mode 100644
index 00000000000..a86852b094f
--- /dev/null
+++ b/app/workers/gitlab/github_import/attachments/import_merge_request_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Attachments
+ class ImportMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::NoteText
+ end
+
+ def importer_class
+ Importer::NoteAttachmentsImporter
+ end
+
+ def object_type
+ :merge_request_attachment
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/attachments/import_note_worker.rb b/app/workers/gitlab/github_import/attachments/import_note_worker.rb
new file mode 100644
index 00000000000..2f5bc50ee0b
--- /dev/null
+++ b/app/workers/gitlab/github_import/attachments/import_note_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Attachments
+ class ImportNoteWorker # rubocop:disable Scalability/IdempotentWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::NoteText
+ end
+
+ def importer_class
+ Importer::NoteAttachmentsImporter
+ end
+
+ def object_type
+ :note_attachment
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/attachments/import_release_worker.rb b/app/workers/gitlab/github_import/attachments/import_release_worker.rb
new file mode 100644
index 00000000000..5eea5702d3c
--- /dev/null
+++ b/app/workers/gitlab/github_import/attachments/import_release_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubImport
+ module Attachments
+ class ImportReleaseWorker # rubocop:disable Scalability/IdempotentWorker
+ include ObjectImporter
+
+ def representation_class
+ Representation::NoteText
+ end
+
+ def importer_class
+ Importer::NoteAttachmentsImporter
+ end
+
+ def object_type
+ :release_attachment
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_import/import_issue_worker.rb b/app/workers/gitlab/github_import/import_issue_worker.rb
index 8fdc0219ffd..7d6a28f4a96 100644
--- a/app/workers/gitlab/github_import/import_issue_worker.rb
+++ b/app/workers/gitlab/github_import/import_issue_worker.rb
@@ -16,6 +16,10 @@ module Gitlab
def object_type
:issue
end
+
+ def increment_object_counter?(object)
+ !object.pull_request?
+ end
end
end
end
diff --git a/app/workers/gitlab/github_import/import_release_attachments_worker.rb b/app/workers/gitlab/github_import/import_release_attachments_worker.rb
index c6f45ec1d7c..bf901f2f7b8 100644
--- a/app/workers/gitlab/github_import/import_release_attachments_worker.rb
+++ b/app/workers/gitlab/github_import/import_release_attachments_worker.rb
@@ -1,16 +1,18 @@
# frozen_string_literal: true
+# TODO: remove in 16.X milestone
+# https://gitlab.com/gitlab-org/gitlab/-/issues/377059
module Gitlab
module GithubImport
class ImportReleaseAttachmentsWorker # rubocop:disable Scalability/IdempotentWorker
include ObjectImporter
def representation_class
- Representation::ReleaseAttachments
+ Representation::NoteText
end
def importer_class
- Importer::ReleaseAttachmentsImporter
+ Importer::NoteAttachmentsImporter
end
def object_type
diff --git a/app/workers/gitlab/github_import/stage/import_attachments_worker.rb b/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
index e9086dca503..e4a413b4081 100644
--- a/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_attachments_worker.rb
@@ -28,7 +28,12 @@ module Gitlab
# For future issue/mr/milestone/etc attachments importers
def importers
- [::Gitlab::GithubImport::Importer::ReleasesAttachmentsImporter]
+ [
+ ::Gitlab::GithubImport::Importer::Attachments::ReleasesImporter,
+ ::Gitlab::GithubImport::Importer::Attachments::NotesImporter,
+ ::Gitlab::GithubImport::Importer::Attachments::IssuesImporter,
+ ::Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter
+ ]
end
def start_importer(project, importer, client)
@@ -50,7 +55,7 @@ module Gitlab
end
def feature_disabled?(project)
- Feature.disabled?(:github_importer_attachments_import, project.group, type: :ops)
+ import_settings(project).disabled?(:attachments_import)
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
index 0ec0a1b58d2..54ed4c47e78 100644
--- a/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_issue_events_worker.rb
@@ -15,9 +15,9 @@ module Gitlab
# client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project.
def import(client, project)
- importer = importer_class(project)
- return skip_to_next_stage(project) if importer.nil?
+ return skip_to_next_stage(project) if import_settings(project).disabled?(:single_endpoint_issue_events_import)
+ importer = ::Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter
info(project.id, message: "starting importer", importer: importer.name)
waiter = importer.new(project, client).execute
move_to_next_stage(project, { waiter.key => waiter.jobs_remaining })
@@ -25,16 +25,6 @@ module Gitlab
private
- def importer_class(project)
- if Feature.enabled?(:github_importer_single_endpoint_issue_events_import, project.group, type: :ops)
- ::Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter
- elsif Feature.enabled?(:github_importer_issue_events_import, project.group, type: :ops)
- ::Gitlab::GithubImport::Importer::IssueEventsImporter
- else
- nil
- end
- end
-
def skip_to_next_stage(project)
info(project.id, message: "skipping importer", importer: "IssueEventsImporter")
move_to_next_stage(project)
diff --git a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
index 7922c1113c4..3d1a8437da2 100644
--- a/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker.rb
@@ -36,7 +36,7 @@ module Gitlab
private
def diff_notes_importer(project)
- if project.group.present? && Feature.enabled?(:github_importer_single_endpoint_notes_import, project.group, type: :ops)
+ if import_settings(project).enabled?(:single_endpoint_notes_import)
Importer::SingleEndpointDiffNotesImporter
else
Importer::DiffNotesImporter
diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
index b53e31ce40e..40ca12b130f 100644
--- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb
@@ -25,7 +25,7 @@ module Gitlab
end
def importers(project)
- if project.group.present? && Feature.enabled?(:github_importer_single_endpoint_notes_import, project.group, type: :ops)
+ if import_settings(project).enabled?(:single_endpoint_notes_import)
[
Importer::SingleEndpointMergeRequestNotesImporter,
Importer::SingleEndpointIssueNotesImporter
diff --git a/app/workers/gitlab/github_import/stage/import_repository_worker.rb b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
index 3e914cc7590..8c1a2cd2677 100644
--- a/app/workers/gitlab/github_import/stage/import_repository_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
@@ -26,6 +26,11 @@ module Gitlab
RefreshImportJidWorker.perform_in_the_future(project.id, jid)
info(project.id, message: "starting importer", importer: 'Importer::RepositoryImporter')
+
+ # If a user creates an issue while the import is in progress, this can lead to an import failure.
+ # The workaround is to allocate IIDs before starting the importer.
+ allocate_issues_internal_id!(project, client)
+
importer = Importer::RepositoryImporter.new(project, client)
importer.execute
@@ -56,6 +61,19 @@ module Gitlab
def abort_on_failure
true
end
+
+ private
+
+ def allocate_issues_internal_id!(project, client)
+ return if InternalId.exists?(project: project, usage: :issues) # rubocop: disable CodeReuse/ActiveRecord
+
+ options = { state: 'all', sort: 'number', direction: 'desc', per_page: '1' }
+ last_github_issue = client.each_object(:issues, project.import_source, options).first
+
+ return unless last_github_issue
+
+ Issue.track_project_iid!(project, last_github_issue[:number])
+ end
end
end
end
diff --git a/app/workers/incident_management/pager_duty/process_incident_worker.rb b/app/workers/incident_management/pager_duty/process_incident_worker.rb
index 933d8e12d25..181e336e6b0 100644
--- a/app/workers/incident_management/pager_duty/process_incident_worker.rb
+++ b/app/workers/incident_management/pager_duty/process_incident_worker.rb
@@ -38,7 +38,7 @@ module IncidentManagement
def log_error(result)
Gitlab::AppLogger.warn(
message: 'Cannot create issue for PagerDuty incident',
- issue_errors: result.message
+ issue_errors: result.errors.join(', ')
)
end
end
diff --git a/app/workers/incident_management/process_alert_worker_v2.rb b/app/workers/incident_management/process_alert_worker_v2.rb
index f3049560bcd..04c02d17704 100644
--- a/app/workers/incident_management/process_alert_worker_v2.rb
+++ b/app/workers/incident_management/process_alert_worker_v2.rb
@@ -37,13 +37,13 @@ module IncidentManagement
end
def log_warning(alert, result)
- issue_id = result.payload[:issue]&.id
+ issue_id = result[:issue]&.id
Gitlab::AppLogger.warn(
message: 'Cannot process an Incident',
issue_id: issue_id,
alert_id: alert.id,
- errors: result.message
+ errors: result.errors.join(', ')
)
end
end
diff --git a/app/workers/merge_requests/delete_source_branch_worker.rb b/app/workers/merge_requests/delete_source_branch_worker.rb
index 69bd3949e9d..66392c670b5 100644
--- a/app/workers/merge_requests/delete_source_branch_worker.rb
+++ b/app/workers/merge_requests/delete_source_branch_worker.rb
@@ -18,9 +18,13 @@ class MergeRequests::DeleteSourceBranchWorker
# Source branch changed while it's being removed
return if merge_request.source_branch_sha != source_branch_sha
- ::Branches::DeleteService.new(merge_request.source_project, user)
+ delete_service_result = ::Branches::DeleteService.new(merge_request.source_project, user)
.execute(merge_request.source_branch)
+ if Feature.enabled?(:track_delete_source_errors, merge_request.source_project)
+ delete_service_result.track_exception if delete_service_result&.error?
+ end
+
::MergeRequests::RetargetChainService.new(project: merge_request.source_project, current_user: user)
.execute(merge_request)
rescue ActiveRecord::RecordNotFound
diff --git a/app/workers/namespaces/onboarding_issue_created_worker.rb b/app/workers/onboarding/issue_created_worker.rb
index 4f0cc71cd91..ff39fefad81 100644
--- a/app/workers/namespaces/onboarding_issue_created_worker.rb
+++ b/app/workers/onboarding/issue_created_worker.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-module Namespaces
- class OnboardingIssueCreatedWorker
+module Onboarding
+ class IssueCreatedWorker
include ApplicationWorker
data_consistency :always
@@ -22,3 +22,6 @@ module Namespaces
end
end
end
+
+# remove in %15.6 as per https://gitlab.com/gitlab-org/gitlab/-/issues/372432
+Namespaces::OnboardingIssueCreatedWorker = Onboarding::IssueCreatedWorker
diff --git a/app/workers/namespaces/onboarding_pipeline_created_worker.rb b/app/workers/onboarding/pipeline_created_worker.rb
index c3850880df0..6bd5863b0e0 100644
--- a/app/workers/namespaces/onboarding_pipeline_created_worker.rb
+++ b/app/workers/onboarding/pipeline_created_worker.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-module Namespaces
- class OnboardingPipelineCreatedWorker
+module Onboarding
+ class PipelineCreatedWorker
include ApplicationWorker
data_consistency :always
@@ -22,3 +22,6 @@ module Namespaces
end
end
end
+
+# remove in %15.6 as per https://gitlab.com/gitlab-org/gitlab/-/issues/372432
+Namespaces::OnboardingPipelineCreatedWorker = Onboarding::PipelineCreatedWorker
diff --git a/app/workers/namespaces/onboarding_progress_worker.rb b/app/workers/onboarding/progress_worker.rb
index 49629428459..525934c4a7c 100644
--- a/app/workers/namespaces/onboarding_progress_worker.rb
+++ b/app/workers/onboarding/progress_worker.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-module Namespaces
- class OnboardingProgressWorker
+module Onboarding
+ class ProgressWorker
include ApplicationWorker
data_consistency :always
@@ -23,3 +23,6 @@ module Namespaces
end
end
end
+
+# remove in %15.6 as per https://gitlab.com/gitlab-org/gitlab/-/issues/372432
+Namespaces::OnboardingProgressWorker = Onboarding::ProgressWorker
diff --git a/app/workers/namespaces/onboarding_user_added_worker.rb b/app/workers/onboarding/user_added_worker.rb
index a1b349eedd3..38e9cd063ea 100644
--- a/app/workers/namespaces/onboarding_user_added_worker.rb
+++ b/app/workers/onboarding/user_added_worker.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-module Namespaces
- class OnboardingUserAddedWorker
+module Onboarding
+ class UserAddedWorker
include ApplicationWorker
data_consistency :always
@@ -19,3 +19,6 @@ module Namespaces
end
end
end
+
+# remove in %15.6 as per https://gitlab.com/gitlab-org/gitlab/-/issues/372432
+Namespaces::OnboardingUserAddedWorker = Onboarding::UserAddedWorker
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index cd6ce6eb28b..708dd3433cb 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -51,23 +51,13 @@ class ProcessCommitWorker
end
def close_issues(project, user, author, commit, issues)
- if Feature.enabled?(:process_issue_closure_in_background, project)
- Issues::CloseWorker.bulk_perform_async_with_contexts(
- issues,
- arguments_proc: -> (issue) {
- [project.id, issue.id, issue.class.to_s, { closed_by: author.id, commit_hash: commit.to_hash }]
- },
- context_proc: -> (issue) { { project: project } }
- )
- else
- # We don't want to run permission related queries for every single issue,
- # therefore we use IssueCollection here and skip the authorization check in
- # Issues::CloseService#execute.
- IssueCollection.new(issues).updatable_by_user(user).each do |issue|
- Issues::CloseService.new(project: project, current_user: author)
- .close_issue(issue, closed_via: commit)
- end
- end
+ Issues::CloseWorker.bulk_perform_async_with_contexts(
+ issues,
+ arguments_proc: -> (issue) {
+ [project.id, issue.id, issue.class.to_s, { closed_by: author.id, commit_hash: commit.to_hash }]
+ },
+ context_proc: -> (issue) { { project: project } }
+ )
end
def issues_to_close(project, commit, user)