summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 18:42:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 18:42:06 +0000
commit6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch)
tree78be5963ec075d80116a932011d695dd33910b4e /app
parent1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff)
downloadgitlab-ce-6e4e1050d9dba2b7b2523fdd1768823ab85feef4.tar.gz
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/mailers/approval/icon-merge-request-gray.gifbin0 -> 704 bytes
-rw-r--r--app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_trigger.vue49
-rw-r--r--app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue279
-rw-r--r--app/assets/javascripts/add_context_commits_modal/components/review_tab_container.vue57
-rw-r--r--app/assets/javascripts/add_context_commits_modal/event_hub.js (renamed from app/assets/javascripts/import_projects/event_hub.js)0
-rw-r--r--app/assets/javascripts/add_context_commits_modal/index.js64
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/actions.js134
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/index.js15
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/mutation_types.js20
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/mutations.js56
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/state.js13
-rw-r--r--app/assets/javascripts/add_context_commits_modal/utils.js32
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/actions.js5
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/getters.js4
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue106
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_empty_state.vue22
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue41
-rw-r--r--app/assets/javascripts/alert_management/components/alert_status.vue14
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue8
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue38
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue2
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue2
-rw-r--r--app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue84
-rw-r--r--app/assets/javascripts/alert_management/details.js5
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql6
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql11
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql10
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql8
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/alert_help_url.query.graphql3
-rw-r--r--app/assets/javascripts/alert_management/list.js9
-rw-r--r--app/assets/javascripts/alert_management/router.js13
-rw-r--r--app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue10
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue148
-rw-r--r--app/assets/javascripts/alerts_settings/constants.js4
-rw-r--r--app/assets/javascripts/alerts_settings/index.js56
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/mixins/filter_mixins.js1
-rw-r--r--app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js28
-rw-r--r--app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue42
-rw-r--r--app/assets/javascripts/api.js70
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue18
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue22
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue38
-rw-r--r--app/assets/javascripts/batch_comments/components/drafts_count.vue10
-rw-r--r--app/assets/javascripts/batch_comments/components/preview_item.vue2
-rw-r--r--app/assets/javascripts/batch_comments/components/publish_button.vue28
-rw-r--r--app/assets/javascripts/batch_comments/components/review_bar.vue12
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js5
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js3
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js4
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js2
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_content_error.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_edit_content.vue37
-rw-r--r--app/assets/javascripts/blob/components/blob_edit_header.vue47
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_header_default_actions.vue32
-rw-r--r--app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue17
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js4
-rw-r--r--app/assets/javascripts/blob/notebook/notebook_viewer.vue2
-rw-r--r--app/assets/javascripts/blob/openapi/index.js2
-rw-r--r--app/assets/javascripts/blob/pdf/pdf_viewer.vue2
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue26
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue12
-rw-r--r--app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js28
-rw-r--r--app/assets/javascripts/blob/utils.js11
-rw-r--r--app/assets/javascripts/blob/viewer/index.js2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js2
-rw-r--r--app/assets/javascripts/boards/boards_util.js21
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue92
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue50
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue6
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue7
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/header.vue2
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.vue2
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js2
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue4
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue10
-rw-r--r--app/assets/javascripts/boards/constants.js2
-rw-r--r--app/assets/javascripts/boards/eventhub.js4
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js3
-rw-r--r--app/assets/javascripts/boards/index.js11
-rw-r--r--app/assets/javascripts/boards/models/list.js15
-rw-r--r--app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql1
-rw-r--r--app/assets/javascripts/boards/queries/group_lists_issues.query.graphql18
-rw-r--r--app/assets/javascripts/boards/queries/issue.fragment.graphql31
-rw-r--r--app/assets/javascripts/boards/queries/project_lists_issues.query.graphql18
-rw-r--r--app/assets/javascripts/boards/stores/actions.js41
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js45
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js6
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js22
-rw-r--r--app/assets/javascripts/boards/stores/state.js8
-rw-r--r--app/assets/javascripts/branches/components/divergence_graph.vue2
-rw-r--r--app/assets/javascripts/branches/components/graph_bar.vue2
-rw-r--r--app/assets/javascripts/branches/divergence_graph.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/ajax_variable_list.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue32
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue18
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue21
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue23
-rw-r--r--app/assets/javascripts/ci_variable_list/store/actions.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js37
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue22
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue101
-rw-r--r--app/assets/javascripts/clusters/components/crossplane_provider_stack.vue18
-rw-r--r--app/assets/javascripts/clusters/components/fluentd_output_settings.vue22
-rw-r--r--app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue26
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue26
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue41
-rw-r--r--app/assets/javascripts/clusters/constants.js1
-rw-r--r--app/assets/javascripts/clusters/forms/components/integration_form.vue163
-rw-r--r--app/assets/javascripts/clusters/forms/show/index.js27
-rw-r--r--app/assets/javascripts/clusters/forms/stores/index.js12
-rw-r--r--app/assets/javascripts/clusters/forms/stores/state.js13
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js36
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js8
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue2
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js7
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue92
-rw-r--r--app/assets/javascripts/compare_autocomplete.js2
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/dropdown.vue18
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue2
-rw-r--r--app/assets/javascripts/contributors/stores/actions.js6
-rw-r--r--app/assets/javascripts/contributors/stores/getters.js3
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/store/actions.js2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue77
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/index.js2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/store/actions.js3
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js2
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue15
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.vue5
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js4
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue149
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue18
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue83
-rw-r--r--app/assets/javascripts/deploy_freeze/index.js22
-rw-r--r--app/assets/javascripts/deploy_freeze/store/actions.js63
-rw-r--r--app/assets/javascripts/deploy_freeze/store/index.js14
-rw-r--r--app/assets/javascripts/deploy_freeze/store/mutation_types.js12
-rw-r--r--app/assets/javascripts/deploy_freeze/store/mutations.js54
-rw-r--r--app/assets/javascripts/deploy_freeze/store/state.js17
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue2
-rw-r--r--app/assets/javascripts/design_management/components/delete_button.vue65
-rw-r--r--app/assets/javascripts/design_management/components/design_destroyer.vue9
-rw-r--r--app/assets/javascripts/design_management/components/design_note_pin.vue8
-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_note.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/toggle_replies_widget.vue6
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue10
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue6
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/design_navigation.vue (renamed from app/assets/javascripts/design_management_new/components/toolbar/pagination.vue)27
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/index.vue78
-rw-r--r--app/assets/javascripts/design_management/components/upload/button.vue11
-rw-r--r--app/assets/javascripts/design_management/components/upload/design_dropzone.vue53
-rw-r--r--app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue65
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql6
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/move_design.mutation.graphql18
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql8
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql22
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql12
-rw-r--r--app/assets/javascripts/design_management/index.js38
-rw-r--r--app/assets/javascripts/design_management/mixins/all_designs.js15
-rw-r--r--app/assets/javascripts/design_management/mixins/all_versions.js25
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue13
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue164
-rw-r--r--app/assets/javascripts/design_management/router/constants.js1
-rw-r--r--app/assets/javascripts/design_management/router/index.js5
-rw-r--r--app/assets/javascripts/design_management/router/routes.js47
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js90
-rw-r--r--app/assets/javascripts/design_management/utils/design_management_utils.js53
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js6
-rw-r--r--app/assets/javascripts/design_management_legacy/components/app.vue (renamed from app/assets/javascripts/design_management_new/components/app.vue)0
-rw-r--r--app/assets/javascripts/design_management_legacy/components/delete_button.vue (renamed from app/assets/javascripts/design_management_new/components/delete_button.vue)37
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_destroyer.vue (renamed from app/assets/javascripts/design_management_new/components/design_destroyer.vue)9
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_note_pin.vue (renamed from app/assets/javascripts/design_management_new/components/design_note_pin.vue)8
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue (renamed from app/assets/javascripts/design_management_new/components/design_notes/design_discussion.vue)4
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue (renamed from app/assets/javascripts/design_management_new/components/design_notes/design_note.vue)4
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue (renamed from app/assets/javascripts/design_management_new/components/design_notes/design_reply_form.vue)0
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue (renamed from app/assets/javascripts/design_management_new/components/design_notes/toggle_replies_widget.vue)6
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_overlay.vue (renamed from app/assets/javascripts/design_management_new/components/design_overlay.vue)0
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_presentation.vue (renamed from app/assets/javascripts/design_management_new/components/design_presentation.vue)0
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_scaler.vue (renamed from app/assets/javascripts/design_management_new/components/design_scaler.vue)0
-rw-r--r--app/assets/javascripts/design_management_legacy/components/design_sidebar.vue (renamed from app/assets/javascripts/design_management_new/components/design_sidebar.vue)8
-rw-r--r--app/assets/javascripts/design_management_legacy/components/image.vue (renamed from app/assets/javascripts/design_management_new/components/image.vue)0
-rw-r--r--app/assets/javascripts/design_management_legacy/components/list/item.vue (renamed from app/assets/javascripts/design_management_new/components/list/item.vue)4
-rw-r--r--app/assets/javascripts/design_management_legacy/components/toolbar/index.vue (renamed from app/assets/javascripts/design_management_new/components/toolbar/index.vue)20
-rw-r--r--app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue (renamed from app/assets/javascripts/design_management/components/toolbar/pagination.vue)0
-rw-r--r--app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue (renamed from app/assets/javascripts/design_management/components/toolbar/pagination_button.vue)0
-rw-r--r--app/assets/javascripts/design_management_legacy/components/upload/button.vue (renamed from app/assets/javascripts/design_management_new/components/upload/button.vue)9
-rw-r--r--app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue (renamed from app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue)40
-rw-r--r--app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue (renamed from app/assets/javascripts/design_management_new/components/upload/design_version_dropdown.vue)16
-rw-r--r--app/assets/javascripts/design_management_legacy/constants.js (renamed from app/assets/javascripts/design_management_new/constants.js)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql.js (renamed from app/assets/javascripts/design_management_new/graphql.js)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql (renamed from app/assets/javascripts/design_management_new/graphql/fragments/design.fragment.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql (renamed from app/assets/javascripts/design_management_new/graphql/fragments/design_list.fragment.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql (renamed from app/assets/javascripts/design_management_new/graphql/fragments/design_note.fragment.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql (renamed from app/assets/javascripts/design_management_new/graphql/fragments/diff_refs.fragment.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql (renamed from app/assets/javascripts/design_management_new/graphql/fragments/discussion_resolved_status.fragment.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql (renamed from app/assets/javascripts/design_management_new/graphql/fragments/note_permissions.fragment.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql (renamed from app/assets/javascripts/design_management_new/graphql/fragments/version.fragment.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql (renamed from app/assets/javascripts/design_management_new/graphql/mutations/create_image_diff_note.mutation.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql (renamed from app/assets/javascripts/design_management_new/graphql/mutations/create_note.mutation.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql (renamed from app/assets/javascripts/design_management_new/graphql/mutations/destroy_design.mutation.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql (renamed from app/assets/javascripts/design_management_new/graphql/mutations/toggle_resolve_discussion.mutation.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql (renamed from app/assets/javascripts/design_management_new/graphql/mutations/update_active_discussion.mutation.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql (renamed from app/assets/javascripts/design_management_new/graphql/mutations/update_image_diff_note.mutation.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql (renamed from app/assets/javascripts/design_management_new/graphql/mutations/update_note.mutation.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql (renamed from app/assets/javascripts/design_management_new/graphql/mutations/upload_design.mutation.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql (renamed from app/assets/javascripts/design_management_new/graphql/queries/active_discussion.query.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql (renamed from app/assets/javascripts/design_management/graphql/queries/app_data.query.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql (renamed from app/assets/javascripts/design_management_new/graphql/queries/design_permissions.query.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql (renamed from app/assets/javascripts/design_management_new/graphql/queries/get_design.query.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql (renamed from app/assets/javascripts/design_management_new/graphql/queries/get_design_list.query.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql (renamed from app/assets/javascripts/design_management_new/graphql/typedefs.graphql)0
-rw-r--r--app/assets/javascripts/design_management_legacy/index.js61
-rw-r--r--app/assets/javascripts/design_management_legacy/mixins/all_designs.js (renamed from app/assets/javascripts/design_management_new/mixins/all_designs.js)2
-rw-r--r--app/assets/javascripts/design_management_legacy/mixins/all_versions.js (renamed from app/assets/javascripts/design_management_new/mixins/all_versions.js)19
-rw-r--r--app/assets/javascripts/design_management_legacy/pages/design/index.vue (renamed from app/assets/javascripts/design_management_new/pages/design/index.vue)13
-rw-r--r--app/assets/javascripts/design_management_legacy/pages/index.vue (renamed from app/assets/javascripts/design_management_new/pages/index.vue)81
-rw-r--r--app/assets/javascripts/design_management_legacy/router/constants.js (renamed from app/assets/javascripts/design_management_new/router/constants.js)1
-rw-r--r--app/assets/javascripts/design_management_legacy/router/index.js (renamed from app/assets/javascripts/design_management_new/router/index.js)7
-rw-r--r--app/assets/javascripts/design_management_legacy/router/routes.js44
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/cache_update.js (renamed from app/assets/javascripts/design_management_new/utils/cache_update.js)2
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/design_management_utils.js (renamed from app/assets/javascripts/design_management_new/utils/design_management_utils.js)0
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/error_messages.js (renamed from app/assets/javascripts/design_management_new/utils/error_messages.js)0
-rw-r--r--app/assets/javascripts/design_management_legacy/utils/tracking.js (renamed from app/assets/javascripts/design_management_new/utils/tracking.js)0
-rw-r--r--app/assets/javascripts/design_management_new/components/toolbar/pagination_button.vue48
-rw-r--r--app/assets/javascripts/design_management_new/index.js33
-rw-r--r--app/assets/javascripts/design_management_new/router/routes.js29
-rw-r--r--app/assets/javascripts/diff.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js2
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue69
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue51
-rw-r--r--app/assets/javascripts/diffs/components/commit_widget.vue9
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue9
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue24
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue13
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue51
-rw-r--r--app/assets/javascripts/diffs/components/hidden_files_warning.vue2
-rw-r--r--app/assets/javascripts/diffs/constants.js3
-rw-r--r--app/assets/javascripts/diffs/diff_file.js28
-rw-r--r--app/assets/javascripts/diffs/index.js2
-rw-r--r--app/assets/javascripts/diffs/store/actions.js9
-rw-r--r--app/assets/javascripts/diffs/store/getters.js4
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js1
-rw-r--r--app/assets/javascripts/diffs/store/utils.js17
-rw-r--r--app/assets/javascripts/dropzone_input.js4
-rw-r--r--app/assets/javascripts/editor/editor_lite.js29
-rw-r--r--app/assets/javascripts/environments/components/enable_review_app_button.vue8
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue14
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue17
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue2
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue2
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js2
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue36
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_actions.vue30
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue55
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue4
-rw-r--r--app/assets/javascripts/error_tracking/store/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/details/actions.js5
-rw-r--r--app/assets/javascripts/error_tracking/store/details/getters.js3
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/app.vue22
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue14
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/actions.js5
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/getters.js3
-rw-r--r--app/assets/javascripts/error_tracking_settings/utils.js2
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js2
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js60
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js2
-rw-r--r--app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue18
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_ajax_filter.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js15
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js4
-rw-r--r--app/assets/javascripts/filtered_search/group_runners_filtered_search_token_keys.js27
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js2
-rw-r--r--app/assets/javascripts/flash.js62
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js3
-rw-r--r--app/assets/javascripts/frequent_items/store/getters.js4
-rw-r--r--app/assets/javascripts/frequent_items/utils.js4
-rw-r--r--app/assets/javascripts/gpg_badges.js2
-rw-r--r--app/assets/javascripts/grafana_integration/components/grafana_integration.vue16
-rw-r--r--app/assets/javascripts/grafana_integration/store/actions.js2
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/pageInfoCursorsOnly.fragment.graphql4
-rw-r--r--app/assets/javascripts/group.js2
-rw-r--r--app/assets/javascripts/group_label_subscription.js2
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue4
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue4
-rw-r--r--app/assets/javascripts/groups_select.js2
-rw-r--r--app/assets/javascripts/helpers/monitor_helper.js3
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue4
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_project_header.vue6
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue4
-rw-r--r--app/assets/javascripts/ide/ide_router.js2
-rw-r--r--app/assets/javascripts/ide/index.js5
-rw-r--r--app/assets/javascripts/ide/lib/editor.js9
-rw-r--r--app/assets/javascripts/ide/stores/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js2
-rw-r--r--app/assets/javascripts/ide/stores/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/branches/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/merge_requests/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/index.js1
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/getters.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/messages.js4
-rw-r--r--app/assets/javascripts/ide/utils.js2
-rw-r--r--app/assets/javascripts/import_projects/components/bitbucket_status_table.vue3
-rw-r--r--app/assets/javascripts/import_projects/components/import_projects_table.vue177
-rw-r--r--app/assets/javascripts/import_projects/components/imported_project_table_row.vue28
-rw-r--r--app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue8
-rw-r--r--app/assets/javascripts/import_projects/components/page_query_param_sync.vue39
-rw-r--r--app/assets/javascripts/import_projects/components/provider_repo_table_row.vue105
-rw-r--r--app/assets/javascripts/import_projects/index.js37
-rw-r--r--app/assets/javascripts/import_projects/store/actions.js142
-rw-r--r--app/assets/javascripts/import_projects/store/getters.js42
-rw-r--r--app/assets/javascripts/import_projects/store/index.js8
-rw-r--r--app/assets/javascripts/import_projects/store/mutation_types.js10
-rw-r--r--app/assets/javascripts/import_projects/store/mutations.js100
-rw-r--r--app/assets/javascripts/import_projects/store/state.js16
-rw-r--r--app/assets/javascripts/import_projects/utils.js7
-rw-r--r--app/assets/javascripts/importer_status.js2
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue407
-rw-r--r--app/assets/javascripts/incidents/constants.js37
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql9
-rw-r--r--app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql52
-rw-r--r--app/assets/javascripts/incidents/list.js44
-rw-r--r--app/assets/javascripts/incidents_settings/components/alerts_form.vue23
-rw-r--r--app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue16
-rw-r--r--app/assets/javascripts/incidents_settings/components/pagerduty_form.vue45
-rw-r--r--app/assets/javascripts/incidents_settings/constants.js3
-rw-r--r--app/assets/javascripts/incidents_settings/incidents_settings_service.js2
-rw-r--r--app/assets/javascripts/integrations/edit/components/active_toggle.vue37
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue4
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue2
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue75
-rw-r--r--app/assets/javascripts/integrations/edit/components/override_dropdown.vue6
-rw-r--r--app/assets/javascripts/integrations/edit/components/trigger_fields.vue2
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js2
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js2
-rw-r--r--app/assets/javascripts/issuable_form.js25
-rw-r--r--app/assets/javascripts/issuable_index.js2
-rw-r--r--app/assets/javascripts/issuables_list/components/issuable.vue154
-rw-r--r--app/assets/javascripts/issuables_list/components/issuables_list_app.vue65
-rw-r--r--app/assets/javascripts/issuables_list/index.js8
-rw-r--r--app/assets/javascripts/issuables_list/service_desk_helper.js55
-rw-r--r--app/assets/javascripts/issue.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/issuable_header_warnings.vue28
-rw-r--r--app/assets/javascripts/issue_show/index.js12
-rw-r--r--app/assets/javascripts/issue_show/utils/parse_data.js2
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_app.vue106
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue159
-rw-r--r--app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql2
-rw-r--r--app/assets/javascripts/jira_import/utils/constants.js29
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue2
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue7
-rw-r--r--app/assets/javascripts/jobs/components/environments_block.vue2
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue22
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue15
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue3
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue68
-rw-r--r--app/assets/javascripts/jobs/store/actions.js5
-rw-r--r--app/assets/javascripts/jobs/store/getters.js3
-rw-r--r--app/assets/javascripts/label_manager.js2
-rw-r--r--app/assets/javascripts/labels_select.js37
-rw-r--r--app/assets/javascripts/layout_nav.js2
-rw-r--r--app/assets/javascripts/lib/chrome_84_icon_fix.js78
-rw-r--r--app/assets/javascripts/lib/graphql.js2
-rw-r--r--app/assets/javascripts/lib/utils/axios_startup_calls.js19
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js4
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js34
-rw-r--r--app/assets/javascripts/lib/utils/highlight.js4
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js1
-rw-r--r--app/assets/javascripts/lib/utils/keys.js4
-rw-r--r--app/assets/javascripts/lib/utils/poll.js17
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js4
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js41
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue38
-rw-r--r--app/assets/javascripts/logs/components/log_simple_filters.vue31
-rw-r--r--app/assets/javascripts/logs/stores/actions.js3
-rw-r--r--app/assets/javascripts/logs/stores/mutations.js4
-rw-r--r--app/assets/javascripts/logs/utils.js2
-rw-r--r--app/assets/javascripts/main.js9
-rw-r--r--app/assets/javascripts/maintenance_mode_settings/components/app.vue6
-rw-r--r--app/assets/javascripts/manual_ordering.js2
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js2
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js2
-rw-r--r--app/assets/javascripts/merge_request.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js14
-rw-r--r--app/assets/javascripts/milestone.js2
-rw-r--r--app/assets/javascripts/milestones/project_milestone_combobox.vue4
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js2
-rw-r--r--app/assets/javascripts/mirrors/mirror_repos.js2
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.js2
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue12
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget_form.vue40
-rw-r--r--app/assets/javascripts/monitoring/components/charts/gauge.vue122
-rw-r--r--app/assets/javascripts/monitoring/components/charts/heatmap.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/options.js64
-rw-r--r--app/assets/javascripts/monitoring/components/charts/single_stat.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue17
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue32
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue291
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue287
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue84
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue199
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue76
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/group_empty_state.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/links_section.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/refresh_button.vue23
-rw-r--r--app/assets/javascripts/monitoring/components/variables/dropdown_field.vue17
-rw-r--r--app/assets/javascripts/monitoring/constants.js11
-rw-r--r--app/assets/javascripts/monitoring/csv_export.js147
-rw-r--r--app/assets/javascripts/monitoring/pages/panel_new_page.vue45
-rw-r--r--app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql1
-rw-r--r--app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql1
-rw-r--r--app/assets/javascripts/monitoring/requests/index.js46
-rw-r--r--app/assets/javascripts/monitoring/router/constants.js9
-rw-r--r--app/assets/javascripts/monitoring/router/routes.js13
-rw-r--r--app/assets/javascripts/monitoring/services/alerts_service.js25
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js130
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/actions.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/getters.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js14
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js76
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js13
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js12
-rw-r--r--app/assets/javascripts/monitoring/utils.js6
-rw-r--r--app/assets/javascripts/monitoring/validators.js13
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js8
-rw-r--r--app/assets/javascripts/mr_notes/stores/getters.js5
-rw-r--r--app/assets/javascripts/mr_tabs_popover/components/popover.vue69
-rw-r--r--app/assets/javascripts/mr_tabs_popover/index.js12
-rw-r--r--app/assets/javascripts/namespaces/leave_by_url.js2
-rw-r--r--app/assets/javascripts/network/branch_graph.js36
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue81
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue7
-rw-r--r--app/assets/javascripts/notes.js11
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_actions.vue11
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue12
-rw-r--r--app/assets/javascripts/notes/components/discussion_navigator.vue (renamed from app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue)6
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue18
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue13
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue7
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue58
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue6
-rw-r--r--app/assets/javascripts/notes/event_hub.js4
-rw-r--r--app/assets/javascripts/notes/index.js5
-rw-r--r--app/assets/javascripts/notes/mixins/diff_line_note_form.js2
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js10
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js54
-rw-r--r--app/assets/javascripts/notes/stores/getters.js7
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js9
-rw-r--r--app/assets/javascripts/notifications_dropdown.js2
-rw-r--r--app/assets/javascripts/notifications_form.js2
-rw-r--r--app/assets/javascripts/onboarding_issues/index.js18
-rw-r--r--app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue2
-rw-r--r--app/assets/javascripts/operation_settings/components/metrics_settings.vue16
-rw-r--r--app/assets/javascripts/operation_settings/store/actions.js5
-rw-r--r--app/assets/javascripts/packages/details/components/additional_metadata.vue98
-rw-r--r--app/assets/javascripts/packages/details/components/app.vue289
-rw-r--r--app/assets/javascripts/packages/details/components/code_instruction.vue63
-rw-r--r--app/assets/javascripts/packages/details/components/composer_installation.vue60
-rw-r--r--app/assets/javascripts/packages/details/components/conan_installation.vue56
-rw-r--r--app/assets/javascripts/packages/details/components/dependency_row.vue35
-rw-r--r--app/assets/javascripts/packages/details/components/history_element.vue35
-rw-r--r--app/assets/javascripts/packages/details/components/installation_commands.vue53
-rw-r--r--app/assets/javascripts/packages/details/components/maven_installation.vue84
-rw-r--r--app/assets/javascripts/packages/details/components/npm_installation.vue80
-rw-r--r--app/assets/javascripts/packages/details/components/nuget_installation.vue55
-rw-r--r--app/assets/javascripts/packages/details/components/package_history.vue114
-rw-r--r--app/assets/javascripts/packages/details/components/package_title.vue112
-rw-r--r--app/assets/javascripts/packages/details/components/pypi_installation.vue68
-rw-r--r--app/assets/javascripts/packages/details/constants.js47
-rw-r--r--app/assets/javascripts/packages/details/index.js32
-rw-r--r--app/assets/javascripts/packages/details/store/actions.js23
-rw-r--r--app/assets/javascripts/packages/details/store/getters.js115
-rw-r--r--app/assets/javascripts/packages/details/store/index.js20
-rw-r--r--app/assets/javascripts/packages/details/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/packages/details/store/mutations.js14
-rw-r--r--app/assets/javascripts/packages/details/utils.js23
-rw-r--r--app/assets/javascripts/packages/list/coming_soon/helpers.js55
-rw-r--r--app/assets/javascripts/packages/list/coming_soon/packages_coming_soon.vue172
-rw-r--r--app/assets/javascripts/packages/list/coming_soon/queries/issues.graphql20
-rw-r--r--app/assets/javascripts/packages/list/components/packages_filter.vue21
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list.vue129
-rw-r--r--app/assets/javascripts/packages/list/components/packages_list_app.vue111
-rw-r--r--app/assets/javascripts/packages/list/components/packages_sort.vue60
-rw-r--r--app/assets/javascripts/packages/list/constants.js101
-rw-r--r--app/assets/javascripts/packages/list/packages_list_app_bundle.js31
-rw-r--r--app/assets/javascripts/packages/list/stores/actions.js73
-rw-r--r--app/assets/javascripts/packages/list/stores/getters.js5
-rw-r--r--app/assets/javascripts/packages/list/stores/index.js20
-rw-r--r--app/assets/javascripts/packages/list/stores/mutation_types.js8
-rw-r--r--app/assets/javascripts/packages/list/stores/mutations.js45
-rw-r--r--app/assets/javascripts/packages/list/stores/state.js57
-rw-r--r--app/assets/javascripts/packages/list/utils.js25
-rw-r--r--app/assets/javascripts/packages/shared/components/package_list_row.vue139
-rw-r--r--app/assets/javascripts/packages/shared/components/package_tags.vue108
-rw-r--r--app/assets/javascripts/packages/shared/components/packages_list_loader.vue86
-rw-r--r--app/assets/javascripts/packages/shared/components/publish_method.vue61
-rw-r--r--app/assets/javascripts/packages/shared/constants.js24
-rw-r--r--app/assets/javascripts/packages/shared/utils.js36
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_previewer.js2
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js2
-rw-r--r--app/assets/javascripts/pages/admin/clusters/show/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue2
-rw-r--r--app/assets/javascripts/pages/admin/runners/index.js1
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue21
-rw-r--r--app/assets/javascripts/pages/dashboard/issues/index.js1
-rw-r--r--app/assets/javascripts/pages/dashboard/merge_requests/index.js1
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue66
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/index.js (renamed from app/assets/javascripts/pages/dashboard/projects/index.js)3
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js16
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js2
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js1
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js1
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js2
-rw-r--r--app/assets/javascripts/pages/groups/packages/index/index.js7
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js5
-rw-r--r--app/assets/javascripts/pages/import/bitbucket/status/index.js4
-rw-r--r--app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue7
-rw-r--r--app/assets/javascripts/pages/import/bitbucket_server/status/index.js6
-rw-r--r--app/assets/javascripts/pages/import/manifest/status/index.js7
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue2
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/clusters/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js50
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue2
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue16
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/index.js24
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue22
-rw-r--r--app/assets/javascripts/pages/projects/incidents/index/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js1
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js1
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js14
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue41
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js1
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js7
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/index/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js17
-rw-r--r--app/assets/javascripts/pages/projects/product_analytics/graphs/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/project.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js1
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/tree/show/index.js2
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js11
-rw-r--r--app/assets/javascripts/pages/search/init_filtered_search.js2
-rw-r--r--app/assets/javascripts/pages/search/show/search.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js14
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js2
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js2
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js2
-rw-r--r--app/assets/javascripts/performance_constants.js12
-rw-r--r--app/assets/javascripts/persistent_user_callout.js2
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js1
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue247
-rw-r--r--app/assets/javascripts/pipeline_new/constants.js2
-rw-r--r--app/assets/javascripts/pipeline_new/index.js36
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue115
-rw-r--r--app/assets/javascripts/pipelines/components/dag/parsing_utils.js50
-rw-r--r--app/assets/javascripts/pipelines/components/graph/action_component.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue20
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue43
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue66
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/stage.vue2
-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.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue2
-rw-r--r--app/assets/javascripts/pipelines/event_hub.js4
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql27
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js69
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_dag.js39
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js45
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js3
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutations.js33
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/state.js10
-rw-r--r--app/assets/javascripts/pipelines/utils.js3
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue1
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue2
-rw-r--r--app/assets/javascripts/profile/profile.js2
-rw-r--r--app/assets/javascripts/project_find_file.js4
-rw-r--r--app/assets/javascripts/project_fork.js9
-rw-r--r--app/assets/javascripts/project_label_subscription.js2
-rw-r--r--app/assets/javascripts/project_select.js2
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js2
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue52
-rw-r--r--app/assets/javascripts/projects/components/remove_modal.vue108
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_button.vue101
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue2
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/project_delete_button.js (renamed from app/assets/javascripts/projects/project_remove_modal.js)9
-rw-r--r--app/assets/javascripts/projects/project_new.js2
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js524
-rw-r--r--app/assets/javascripts/projects/settings/constants.js13
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue10
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/prometheus_alerts/components/reset_key.vue31
-rw-r--r--app/assets/javascripts/prometheus_metrics/prometheus_metrics.js9
-rw-r--r--app/assets/javascripts/protected_branches/constants.js18
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js28
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js109
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js171
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js1
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js2
-rw-r--r--app/assets/javascripts/ref/components/ref_results_section.vue2
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue16
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue2
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_item.vue4
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue8
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue4
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue2
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue2
-rw-r--r--app/assets/javascripts/registry/explorer/pages/index.vue4
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue2
-rw-r--r--app/assets/javascripts/registry/explorer/stores/actions.js4
-rw-r--r--app/assets/javascripts/registry/settings/store/actions.js3
-rw-r--r--app/assets/javascripts/registry/shared/components/details_row.vue (renamed from app/assets/javascripts/registry/explorer/components/details_page/details_row.vue)18
-rw-r--r--app/assets/javascripts/related_merge_requests/store/actions.js5
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue (renamed from app/assets/javascripts/releases/components/app_edit.vue)86
-rw-r--r--app/assets/javascripts/releases/components/app_new.vue9
-rw-r--r--app/assets/javascripts/releases/components/asset_links_form.vue14
-rw-r--r--app/assets/javascripts/releases/components/form_field_container.vue12
-rw-r--r--app/assets/javascripts/releases/components/release_block_assets.vue4
-rw-r--r--app/assets/javascripts/releases/components/release_block_author.vue2
-rw-r--r--app/assets/javascripts/releases/components/release_block_milestone_info.vue2
-rw-r--r--app/assets/javascripts/releases/components/tag_field.vue20
-rw-r--r--app/assets/javascripts/releases/components/tag_field_existing.vue51
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue100
-rw-r--r--app/assets/javascripts/releases/mount_edit.js4
-rw-r--r--app/assets/javascripts/releases/mount_new.js7
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/actions.js167
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/getters.js15
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutation_types.js10
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutations.js26
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/state.js10
-rw-r--r--app/assets/javascripts/releases/stores/modules/list/actions.js5
-rw-r--r--app/assets/javascripts/releases/util.js41
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/actions.js3
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/getters.js3
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue5
-rw-r--r--app/assets/javascripts/reports/components/modal.vue2
-rw-r--r--app/assets/javascripts/reports/components/modal_open_name.vue22
-rw-r--r--app/assets/javascripts/reports/store/actions.js3
-rw-r--r--app/assets/javascripts/reports/store/getters.js4
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue37
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue22
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue8
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue6
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue4
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue46
-rw-r--r--app/assets/javascripts/repository/index.js3
-rw-r--r--app/assets/javascripts/repository/log_tree.js14
-rw-r--r--app/assets/javascripts/repository/mixins/get_ref.js4
-rw-r--r--app/assets/javascripts/repository/mixins/preload.js8
-rw-r--r--app/assets/javascripts/repository/queries/commit.query.graphql (renamed from app/assets/javascripts/repository/queries/getCommit.query.graphql)0
-rw-r--r--app/assets/javascripts/repository/queries/commits.query.graphql (renamed from app/assets/javascripts/repository/queries/getCommits.query.graphql)0
-rw-r--r--app/assets/javascripts/repository/queries/files.query.graphql (renamed from app/assets/javascripts/repository/queries/getFiles.query.graphql)4
-rw-r--r--app/assets/javascripts/repository/queries/path_last_commit.query.graphql (renamed from app/assets/javascripts/repository/queries/pathLastCommit.query.graphql)6
-rw-r--r--app/assets/javascripts/repository/queries/permissions.query.graphql (renamed from app/assets/javascripts/repository/queries/getPermissions.query.graphql)0
-rw-r--r--app/assets/javascripts/repository/queries/project_path.query.graphql (renamed from app/assets/javascripts/repository/queries/getProjectPath.query.graphql)0
-rw-r--r--app/assets/javascripts/repository/queries/project_short_path.query.graphql (renamed from app/assets/javascripts/repository/queries/getProjectShortPath.query.graphql)0
-rw-r--r--app/assets/javascripts/repository/queries/readme.query.graphql (renamed from app/assets/javascripts/repository/queries/getReadme.query.graphql)0
-rw-r--r--app/assets/javascripts/repository/queries/ref.query.graphql (renamed from app/assets/javascripts/repository/queries/getRef.query.graphql)0
-rw-r--r--app/assets/javascripts/right_sidebar.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js10
-rw-r--r--app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue2
-rw-r--r--app/assets/javascripts/serverless/components/empty_state.vue62
-rw-r--r--app/assets/javascripts/serverless/components/function_details.vue10
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue27
-rw-r--r--app/assets/javascripts/serverless/components/missing_prometheus.vue10
-rw-r--r--app/assets/javascripts/serverless/serverless_bundle.js26
-rw-r--r--app/assets/javascripts/serverless/store/actions.js5
-rw-r--r--app/assets/javascripts/serverless/store/getters.js3
-rw-r--r--app/assets/javascripts/serverless/store/index.js6
-rw-r--r--app/assets/javascripts/serverless/store/state.js10
-rw-r--r--app/assets/javascripts/serverless/survey_banner.vue2
-rw-r--r--app/assets/javascripts/serverless/utils.js3
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue60
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue38
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue38
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql (renamed from app/assets/javascripts/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql)1
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue62
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue57
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue129
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue140
-rw-r--r--app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql8
-rw-r--r--app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql8
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js33
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js4
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js2
-rw-r--r--app/assets/javascripts/single_file_diff.js2
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue87
-rw-r--r--app/assets/javascripts/snippets/components/show.vue25
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue156
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue104
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue28
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue35
-rw-r--r--app/assets/javascripts/snippets/constants.js3
-rw-r--r--app/assets/javascripts/snippets/index.js4
-rw-r--r--app/assets/javascripts/snippets/mixins/snippets.js3
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql3
-rw-r--r--app/assets/javascripts/snippets/utils/blob.js66
-rw-r--r--app/assets/javascripts/star.js2
-rw-r--r--app/assets/javascripts/static_site_editor/components/app.vue12
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue18
-rw-r--r--app/assets/javascripts/static_site_editor/components/saved_changes_message.vue79
-rw-r--r--app/assets/javascripts/static_site_editor/image_repository.js2
-rw-r--r--app/assets/javascripts/static_site_editor/index.js15
-rw-r--r--app/assets/javascripts/static_site_editor/pages/home.vue2
-rw-r--r--app/assets/javascripts/static_site_editor/pages/success.vue72
-rw-r--r--app/assets/javascripts/static_site_editor/services/formatter.js14
-rw-r--r--app/assets/javascripts/static_site_editor/services/templater.js89
-rw-r--r--app/assets/javascripts/task_list.js2
-rw-r--r--app/assets/javascripts/toggle_buttons.js2
-rw-r--r--app/assets/javascripts/usage_ping_consent.js2
-rw-r--r--app/assets/javascripts/user_popovers.js2
-rw-r--r--app/assets/javascripts/users_select/index.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue75
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue73
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue37
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue156
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/merge_request_query_variables.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js27
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_modal.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_container.vue54
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue62
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue126
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue110
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_mentions.vue224
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue41
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js61
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js39
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue26
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown.vue102
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue14
-rw-r--r--app/assets/javascripts/vue_shared/constants.js4
-rw-r--r--app/assets/javascripts/vuex_shared/bindings.js3
-rw-r--r--app/assets/javascripts/vuex_shared/modules/modal/actions.js3
-rw-r--r--app/assets/javascripts/whats_new/components/app.vue29
-rw-r--r--app/assets/javascripts/whats_new/components/trigger.vue19
-rw-r--r--app/assets/javascripts/whats_new/index.js32
-rw-r--r--app/assets/javascripts/whats_new/store/actions.js10
-rw-r--r--app/assets/javascripts/whats_new/store/index.js13
-rw-r--r--app/assets/javascripts/whats_new/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/whats_new/store/mutations.js10
-rw-r--r--app/assets/javascripts/whats_new/store/state.js3
-rw-r--r--app/assets/stylesheets/application.scss8
-rw-r--r--app/assets/stylesheets/components/avatar.scss2
-rw-r--r--app/assets/stylesheets/components/dashboard_skeleton.scss6
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss4
-rw-r--r--app/assets/stylesheets/components/design_management/design_list_item.scss9
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss24
-rw-r--r--app/assets/stylesheets/components/rich_content_editor.scss6
-rw-r--r--app/assets/stylesheets/fontawesome_custom.scss332
-rw-r--r--app/assets/stylesheets/framework/awards.scss2
-rw-r--r--app/assets/stylesheets/framework/badges.scss2
-rw-r--r--app/assets/stylesheets/framework/broadcast_messages.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss6
-rw-r--r--app/assets/stylesheets/framework/ci_variable_list.scss3
-rw-r--r--app/assets/stylesheets/framework/common.scss12
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss1
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss6
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/filters.scss42
-rw-r--r--app/assets/stylesheets/framework/forms.scss2
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss12
-rw-r--r--app/assets/stylesheets/framework/icons.scss6
-rw-r--r--app/assets/stylesheets/framework/images.scss8
-rw-r--r--app/assets/stylesheets/framework/lists.scss6
-rw-r--r--app/assets/stylesheets/framework/mixins.scss4
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss2
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss4
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss7
-rw-r--r--app/assets/stylesheets/framework/snippets.scss4
-rw-r--r--app/assets/stylesheets/framework/spinner.scss2
-rw-r--r--app/assets/stylesheets/framework/stacked_progress_bar.scss4
-rw-r--r--app/assets/stylesheets/framework/tables.scss4
-rw-r--r--app/assets/stylesheets/framework/toggle.scss51
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss31
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss11
-rw-r--r--app/assets/stylesheets/pages/alert_management/details.scss6
-rw-r--r--app/assets/stylesheets/pages/alert_management/severity-icons.scss4
-rw-r--r--app/assets/stylesheets/pages/boards.scss34
-rw-r--r--app/assets/stylesheets/pages/builds.scss4
-rw-r--r--app/assets/stylesheets/pages/clusters.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss3
-rw-r--r--app/assets/stylesheets/pages/editor.scss11
-rw-r--r--app/assets/stylesheets/pages/environments.scss9
-rw-r--r--app/assets/stylesheets/pages/graph.scss4
-rw-r--r--app/assets/stylesheets/pages/incident_management_list.scss (renamed from app/assets/stylesheets/pages/alert_management/list.scss)88
-rw-r--r--app/assets/stylesheets/pages/issuable.scss20
-rw-r--r--app/assets/stylesheets/pages/labels.scss4
-rw-r--r--app/assets/stylesheets/pages/members.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss7
-rw-r--r--app/assets/stylesheets/pages/note_form.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss29
-rw-r--r--app/assets/stylesheets/pages/packages.scss11
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss39
-rw-r--r--app/assets/stylesheets/pages/projects.scss35
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss21
-rw-r--r--app/assets/stylesheets/pages/reports.scss2
-rw-r--r--app/assets/stylesheets/pages/runners.scss4
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/settings.scss5
-rw-r--r--app/assets/stylesheets/pages/status.scss4
-rw-r--r--app/assets/stylesheets/pages/todos.scss4
-rw-r--r--app/assets/stylesheets/pages/tree.scss6
-rw-r--r--app/assets/stylesheets/pages/users.scss6
-rw-r--r--app/assets/stylesheets/startup/_cloaking.scss13
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss5
-rw-r--r--app/assets/stylesheets/utilities.scss10
-rw-r--r--app/controllers/admin/application_settings_controller.rb10
-rw-r--r--app/controllers/admin/integrations_controller.rb2
-rw-r--r--app/controllers/admin/services_controller.rb3
-rw-r--r--app/controllers/clusters/base_controller.rb4
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb12
-rw-r--r--app/controllers/concerns/checks_collaboration.rb2
-rw-r--r--app/controllers/concerns/graceful_timeout_handling.rb15
-rw-r--r--app/controllers/concerns/integrations_actions.rb3
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/concerns/packages_access.rb20
-rw-r--r--app/controllers/concerns/paginated_collection.rb2
-rw-r--r--app/controllers/concerns/renders_blob.rb8
-rw-r--r--app/controllers/concerns/send_file_upload.rb21
-rw-r--r--app/controllers/concerns/snippets_actions.rb22
-rw-r--r--app/controllers/concerns/wiki_actions.rb10
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/explore/projects_controller.rb2
-rw-r--r--app/controllers/groups/packages_controller.rb13
-rw-r--r--app/controllers/groups/releases_controller.rb23
-rw-r--r--app/controllers/groups/variables_controller.rb7
-rw-r--r--app/controllers/import/available_namespaces_controller.rb7
-rw-r--r--app/controllers/import/base_controller.rb8
-rw-r--r--app/controllers/import/gitea_controller.rb10
-rw-r--r--app/controllers/import/github_controller.rb76
-rw-r--r--app/controllers/import/manifest_controller.rb57
-rw-r--r--app/controllers/invites_controller.rb46
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb7
-rw-r--r--app/controllers/profiles/passwords_controller.rb2
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb8
-rw-r--r--app/controllers/projects/artifacts_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/ci/lints_controller.rb34
-rw-r--r--app/controllers/projects/commit_controller.rb8
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb1
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb1
-rw-r--r--app/controllers/projects/environments_controller.rb15
-rw-r--r--app/controllers/projects/forks_controller.rb14
-rw-r--r--app/controllers/projects/incidents_controller.rb8
-rw-r--r--app/controllers/projects/issues_controller.rb20
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb19
-rw-r--r--app/controllers/projects/metrics/dashboards/builder_controller.rb42
-rw-r--r--app/controllers/projects/metrics_dashboard_controller.rb3
-rw-r--r--app/controllers/projects/packages/package_files_controller.rb16
-rw-r--r--app/controllers/projects/packages/packages_controller.rb24
-rw-r--r--app/controllers/projects/pipelines/tests_controller.rb19
-rw-r--r--app/controllers/projects/pipelines_controller.rb11
-rw-r--r--app/controllers/projects/product_analytics_controller.rb53
-rw-r--r--app/controllers/projects/prometheus/alerts_controller.rb8
-rw-r--r--app/controllers/projects/protected_refs_controller.rb2
-rw-r--r--app/controllers/projects/releases_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb1
-rw-r--r--app/controllers/projects/settings/operations_controller.rb6
-rw-r--r--app/controllers/projects/snippets/blobs_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb4
-rw-r--r--app/controllers/projects/variables_controller.rb7
-rw-r--r--app/controllers/projects_controller.rb8
-rw-r--r--app/controllers/registrations/experience_levels_controller.rb4
-rw-r--r--app/controllers/registrations_controller.rb14
-rw-r--r--app/controllers/repositories/git_http_controller.rb2
-rw-r--r--app/controllers/repositories/lfs_storage_controller.rb5
-rw-r--r--app/controllers/root_controller.rb5
-rw-r--r--app/controllers/search_controller.rb14
-rw-r--r--app/controllers/sessions_controller.rb7
-rw-r--r--app/controllers/snippets_controller.rb4
-rw-r--r--app/finders/ci/daily_build_group_report_results_finder.rb16
-rw-r--r--app/finders/concerns/merged_at_filter.rb33
-rw-r--r--app/finders/context_commits_finder.rb4
-rw-r--r--app/finders/design_management/designs_finder.rb7
-rw-r--r--app/finders/group_members_finder.rb2
-rw-r--r--app/finders/issues_finder.rb10
-rw-r--r--app/finders/members_finder.rb8
-rw-r--r--app/finders/merge_requests_finder.rb27
-rw-r--r--app/finders/milestones_finder.rb8
-rw-r--r--app/finders/personal_access_tokens_finder.rb13
-rw-r--r--app/finders/releases_finder.rb41
-rw-r--r--app/finders/template_finder.rb9
-rw-r--r--app/finders/todos_finder.rb10
-rw-r--r--app/graphql/gitlab_schema.rb16
-rw-r--r--app/graphql/mutations/boards/issues/issue_move_list.rb91
-rw-r--r--app/graphql/mutations/boards/lists/base.rb28
-rw-r--r--app/graphql/mutations/boards/lists/create.rb73
-rw-r--r--app/graphql/mutations/boards/lists/update.rb52
-rw-r--r--app/graphql/mutations/concerns/mutations/assignable.rb52
-rw-r--r--app/graphql/mutations/concerns/mutations/resolves_subscription.rb25
-rw-r--r--app/graphql/mutations/design_management/move.rb46
-rw-r--r--app/graphql/mutations/issues/base.rb2
-rw-r--r--app/graphql/mutations/issues/set_assignees.rb15
-rw-r--r--app/graphql/mutations/issues/set_subscription.rb11
-rw-r--r--app/graphql/mutations/issues/update.rb21
-rw-r--r--app/graphql/mutations/merge_requests/create.rb17
-rw-r--r--app/graphql/mutations/merge_requests/set_assignees.rb39
-rw-r--r--app/graphql/mutations/merge_requests/set_subscription.rb17
-rw-r--r--app/graphql/mutations/notes/update/base.rb2
-rw-r--r--app/graphql/mutations/notes/update/note.rb7
-rw-r--r--app/graphql/mutations/snippets/create.rb8
-rw-r--r--app/graphql/mutations/snippets/update.rb8
-rw-r--r--app/graphql/resolvers/board_list_issues_resolver.rb19
-rw-r--r--app/graphql/resolvers/board_lists_resolver.rb23
-rw-r--r--app/graphql/resolvers/ci/pipeline_stages_resolver.rb21
-rw-r--r--app/graphql/resolvers/ci_configuration/sast_resolver.rb17
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_fields.rb77
-rw-r--r--app/graphql/resolvers/concerns/resolves_merge_requests.rb3
-rw-r--r--app/graphql/resolvers/design_management/designs_resolver.rb9
-rw-r--r--app/graphql/resolvers/group_issues_resolver.rb10
-rw-r--r--app/graphql/resolvers/group_milestones_resolver.rb28
-rw-r--r--app/graphql/resolvers/issue_status_counts_resolver.rb13
-rw-r--r--app/graphql/resolvers/issues_resolver.rb61
-rw-r--r--app/graphql/resolvers/merge_requests_resolver.rb6
-rw-r--r--app/graphql/resolvers/milestone_resolver.rb71
-rw-r--r--app/graphql/resolvers/milestones_resolver.rb55
-rw-r--r--app/graphql/resolvers/project_milestones_resolver.rb20
-rw-r--r--app/graphql/resolvers/project_pipeline_resolver.rb2
-rw-r--r--app/graphql/resolvers/projects/jira_projects_resolver.rb9
-rw-r--r--app/graphql/resolvers/todo_resolver.rb16
-rw-r--r--app/graphql/types/alert_management/alert_type.rb26
-rw-r--r--app/graphql/types/board_list_type.rb27
-rw-r--r--app/graphql/types/ci/group_type.rb17
-rw-r--r--app/graphql/types/ci/job_type.rb15
-rw-r--r--app/graphql/types/ci/pipeline_config_source_enum.rb11
-rw-r--r--app/graphql/types/ci/pipeline_type.rb13
-rw-r--r--app/graphql/types/ci/stage_type.rb15
-rw-r--r--app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb25
-rw-r--r--app/graphql/types/ci_configuration/sast/entity_type.rb34
-rw-r--r--app/graphql/types/ci_configuration/sast/options_entity_type.rb19
-rw-r--r--app/graphql/types/ci_configuration/sast/type.rb22
-rw-r--r--app/graphql/types/commit_type.rb3
-rw-r--r--app/graphql/types/countable_connection_type.rb24
-rw-r--r--app/graphql/types/environment_type.rb5
-rw-r--r--app/graphql/types/group_type.rb6
-rw-r--r--app/graphql/types/issuable_state_enum.rb1
-rw-r--r--app/graphql/types/issue_connection_type.rb13
-rw-r--r--app/graphql/types/issue_status_counts_type.rb23
-rw-r--r--app/graphql/types/issue_type.rb6
-rw-r--r--app/graphql/types/issue_type_enum.rb12
-rw-r--r--app/graphql/types/merge_request_type.rb13
-rw-r--r--app/graphql/types/mutation_type.rb6
-rw-r--r--app/graphql/types/project_type.rb18
-rw-r--r--app/graphql/types/projects/services/jira_service_type.rb2
-rw-r--r--app/graphql/types/prometheus_alert_type.rb20
-rw-r--r--app/graphql/types/query_type.rb9
-rw-r--r--app/graphql/types/snippet_type.rb3
-rw-r--r--app/graphql/types/snippets/blob_action_enum.rb (renamed from app/graphql/types/snippets/file_input_action_enum.rb)6
-rw-r--r--app/graphql/types/snippets/blob_action_input_type.rb (renamed from app/graphql/types/snippets/file_input_type.rb)6
-rw-r--r--app/graphql/types/time_type.rb2
-rw-r--r--app/graphql/types/tree/blob_type.rb2
-rw-r--r--app/graphql/types/tree/tree_entry_type.rb2
-rw-r--r--app/graphql/types/user_status_type.rb15
-rw-r--r--app/graphql/types/user_type.rb6
-rw-r--r--app/helpers/active_sessions_helper.rb2
-rw-r--r--app/helpers/application_helper.rb16
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/award_emoji_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb36
-rw-r--r--app/helpers/branches_helper.rb8
-rw-r--r--app/helpers/ci/pipelines_helper.rb18
-rw-r--r--app/helpers/clusters_helper.rb12
-rw-r--r--app/helpers/custom_metrics_helper.rb4
-rw-r--r--app/helpers/dashboard_helper.rb2
-rw-r--r--app/helpers/environment_helper.rb2
-rw-r--r--app/helpers/environments_helper.rb7
-rw-r--r--app/helpers/events_helper.rb3
-rw-r--r--app/helpers/form_helper.rb2
-rw-r--r--app/helpers/gitlab_routing_helper.rb30
-rw-r--r--app/helpers/graph_helper.rb2
-rw-r--r--app/helpers/groups_helper.rb12
-rw-r--r--app/helpers/icons_helper.rb25
-rw-r--r--app/helpers/import_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb9
-rw-r--r--app/helpers/issues_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb6
-rw-r--r--app/helpers/mirror_helper.rb2
-rw-r--r--app/helpers/namespace_storage_limit_alert_helper.rb9
-rw-r--r--app/helpers/notes_helper.rb16
-rw-r--r--app/helpers/notifications_helper.rb4
-rw-r--r--app/helpers/operations_helper.rb2
-rw-r--r--app/helpers/packages_helper.rb59
-rw-r--r--app/helpers/preferences_helper.rb2
-rw-r--r--app/helpers/product_analytics_helper.rb11
-rw-r--r--app/helpers/projects/alert_management_helper.rb3
-rw-r--r--app/helpers/projects/incidents_helper.rb16
-rw-r--r--app/helpers/projects/issues/service_desk_helper.rb35
-rw-r--r--app/helpers/projects_helper.rb44
-rw-r--r--app/helpers/releases_helper.rb5
-rw-r--r--app/helpers/search_helper.rb1
-rw-r--r--app/helpers/services_helper.rb27
-rw-r--r--app/helpers/snippets_helper.rb15
-rw-r--r--app/helpers/sorting_helper.rb43
-rw-r--r--app/helpers/timeboxes_helper.rb6
-rw-r--r--app/helpers/todos_helper.rb3
-rw-r--r--app/helpers/user_callouts_helper.rb12
-rw-r--r--app/helpers/wiki_helper.rb2
-rw-r--r--app/mailers/emails/profile.rb11
-rw-r--r--app/models/alert_management/alert.rb2
-rw-r--r--app/models/application_record.rb8
-rw-r--r--app/models/application_setting.rb10
-rw-r--r--app/models/application_setting_implementation.rb70
-rw-r--r--app/models/audit_event.rb8
-rw-r--r--app/models/audit_event_partitioned.rb14
-rw-r--r--app/models/blob.rb2
-rw-r--r--app/models/ci/build.rb68
-rw-r--r--app/models/ci/build_trace_chunk.rb39
-rw-r--r--app/models/ci/build_trace_chunks/database.rb18
-rw-r--r--app/models/ci/build_trace_chunks/fog.rb20
-rw-r--r--app/models/ci/build_trace_chunks/redis.rb38
-rw-r--r--app/models/ci/group.rb12
-rw-r--r--app/models/ci/instance_variable.rb10
-rw-r--r--app/models/ci/job_artifact.rb41
-rw-r--r--app/models/ci/legacy_stage.rb2
-rw-r--r--app/models/ci/pipeline.rb86
-rw-r--r--app/models/ci/pipeline_artifact.rb37
-rw-r--r--app/models/ci/pipeline_enums.rb4
-rw-r--r--app/models/ci/ref.rb12
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/ci/stage.rb4
-rw-r--r--app/models/clusters/agent.rb20
-rw-r--r--app/models/clusters/agent_token.rb14
-rw-r--r--app/models/clusters/applications/cert_manager.rb6
-rw-r--r--app/models/clusters/applications/crossplane.rb3
-rw-r--r--app/models/clusters/applications/elastic_stack.rb9
-rw-r--r--app/models/clusters/applications/fluentd.rb3
-rw-r--r--app/models/clusters/applications/helm.rb6
-rw-r--r--app/models/clusters/applications/ingress.rb5
-rw-r--r--app/models/clusters/applications/jupyter.rb3
-rw-r--r--app/models/clusters/applications/knative.rb6
-rw-r--r--app/models/clusters/applications/prometheus.rb9
-rw-r--r--app/models/clusters/applications/runner.rb5
-rw-r--r--app/models/clusters/cluster.rb31
-rw-r--r--app/models/clusters/concerns/application_core.rb2
-rw-r--r--app/models/clusters/concerns/application_data.rb17
-rw-r--r--app/models/clusters/concerns/application_status.rb20
-rw-r--r--app/models/clusters/providers/aws.rb3
-rw-r--r--app/models/commit.rb1
-rw-r--r--app/models/commit_collection.rb5
-rw-r--r--app/models/commit_status.rb30
-rw-r--r--app/models/commit_status_enums.rb3
-rw-r--r--app/models/concerns/avatarable.rb11
-rw-r--r--app/models/concerns/cache_markdown_field.rb8
-rw-r--r--app/models/concerns/ci/artifactable.rb20
-rw-r--r--app/models/concerns/ci/contextable.rb20
-rw-r--r--app/models/concerns/ci/has_status.rb58
-rw-r--r--app/models/concerns/counter_attribute.rb143
-rw-r--r--app/models/concerns/file_store_mounter.rb21
-rw-r--r--app/models/concerns/has_wiki.rb2
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/milestoneable.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb403
-rw-r--r--app/models/concerns/sha_attribute.rb13
-rw-r--r--app/models/concerns/time_trackable.rb2
-rw-r--r--app/models/concerns/triggerable_hooks.rb3
-rw-r--r--app/models/concerns/update_project_statistics.rb2
-rw-r--r--app/models/deployment.rb1
-rw-r--r--app/models/design_management/design.rb36
-rw-r--r--app/models/design_management/design_collection.rb4
-rw-r--r--app/models/discussion.rb1
-rw-r--r--app/models/environment.rb5
-rw-r--r--app/models/event.rb7
-rw-r--r--app/models/experiment.rb27
-rw-r--r--app/models/experiment_user.rb10
-rw-r--r--app/models/external_pull_request.rb2
-rw-r--r--app/models/group.rb10
-rw-r--r--app/models/group_deploy_key.rb22
-rw-r--r--app/models/group_deploy_keys_group.rb10
-rw-r--r--app/models/hooks/project_hook.rb3
-rw-r--r--app/models/individual_note_discussion.rb8
-rw-r--r--app/models/issue.rb31
-rw-r--r--app/models/iteration.rb8
-rw-r--r--app/models/lfs_object.rb11
-rw-r--r--app/models/member.rb1
-rw-r--r--app/models/merge_request.rb40
-rw-r--r--app/models/merge_request/metrics.rb13
-rw-r--r--app/models/merge_request_context_commit.rb3
-rw-r--r--app/models/merge_request_diff.rb47
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/network/graph.rb8
-rw-r--r--app/models/note.rb8
-rw-r--r--app/models/notification_recipient.rb13
-rw-r--r--app/models/notification_setting.rb9
-rw-r--r--app/models/packages/package_file.rb15
-rw-r--r--app/models/pages_domain.rb10
-rw-r--r--app/models/personal_access_token.rb1
-rw-r--r--app/models/personal_snippet.rb4
-rw-r--r--app/models/postgresql/replication_slot.rb2
-rw-r--r--app/models/product_analytics_event.rb8
-rw-r--r--app/models/project.rb70
-rw-r--r--app/models/project_repository_storage_move.rb13
-rw-r--r--app/models/project_services/buildkite_service.rb37
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb45
-rw-r--r--app/models/project_services/jira_service.rb7
-rw-r--r--app/models/project_services/jira_tracker_data.rb2
-rw-r--r--app/models/project_services/prometheus_service.rb8
-rw-r--r--app/models/prometheus_alert.rb8
-rw-r--r--app/models/raw_usage_data.rb10
-rw-r--r--app/models/release.rb10
-rw-r--r--app/models/releases/link.rb2
-rw-r--r--app/models/repository.rb52
-rw-r--r--app/models/resource_iteration_event.rb5
-rw-r--r--app/models/resource_milestone_event.rb19
-rw-r--r--app/models/resource_timebox_event.rb23
-rw-r--r--app/models/service.rb19
-rw-r--r--app/models/suggestion.rb22
-rw-r--r--app/models/terraform/state.rb17
-rw-r--r--app/models/user.rb17
-rw-r--r--app/models/user_callout_enums.rb4
-rw-r--r--app/models/wiki.rb1
-rw-r--r--app/models/wiki_page.rb28
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/policies/ci/pipeline_policy.rb2
-rw-r--r--app/policies/concerns/crud_policy_helpers.rb10
-rw-r--r--app/policies/concerns/readonly_abilities.rb52
-rw-r--r--app/policies/group_deploy_key_policy.rb8
-rw-r--r--app/policies/group_deploy_keys_group_policy.rb9
-rw-r--r--app/policies/group_policy.rb1
-rw-r--r--app/policies/issue_policy.rb7
-rw-r--r--app/policies/personal_access_token_policy.rb10
-rw-r--r--app/policies/project_policy.rb54
-rw-r--r--app/policies/prometheus_alert_policy.rb5
-rw-r--r--app/policies/suggestion_policy.rb2
-rw-r--r--app/policies/user_policy.rb1
-rw-r--r--app/presenters/alert_management/alert_presenter.rb12
-rw-r--r--app/presenters/alert_management/prometheus_alert_presenter.rb6
-rw-r--r--app/presenters/blob_presenter.rb4
-rw-r--r--app/presenters/ci/build_runner_presenter.rb2
-rw-r--r--app/presenters/clusters/cluster_presenter.rb2
-rw-r--r--app/presenters/commit_presenter.rb4
-rw-r--r--app/presenters/commit_status_presenter.rb3
-rw-r--r--app/presenters/event_presenter.rb2
-rw-r--r--app/presenters/gitlab/blame_presenter.rb2
-rw-r--r--app/presenters/issue_presenter.rb4
-rw-r--r--app/presenters/merge_request_presenter.rb6
-rw-r--r--app/presenters/packages/detail/package_presenter.rb6
-rw-r--r--app/presenters/project_presenter.rb2
-rw-r--r--app/presenters/projects/prometheus/alert_presenter.rb12
-rw-r--r--app/presenters/prometheus_alert_presenter.rb18
-rw-r--r--app/presenters/snippet_blob_presenter.rb15
-rw-r--r--app/presenters/snippet_presenter.rb6
-rw-r--r--app/presenters/tree_entry_presenter.rb4
-rw-r--r--app/presenters/user_presenter.rb4
-rw-r--r--app/serializers/build_details_entity.rb10
-rw-r--r--app/serializers/cluster_entity.rb4
-rw-r--r--app/serializers/cluster_error_entity.rb7
-rw-r--r--app/serializers/cluster_serializer.rb1
-rw-r--r--app/serializers/diffs_metadata_entity.rb19
-rw-r--r--app/serializers/discussion_entity.rb2
-rw-r--r--app/serializers/environment_entity.rb6
-rw-r--r--app/serializers/group_basic_entity.rb10
-rw-r--r--app/serializers/group_deploy_key_entity.rb17
-rw-r--r--app/serializers/group_deploy_key_serializer.rb5
-rw-r--r--app/serializers/group_deploy_keys_group_entity.rb6
-rw-r--r--app/serializers/import/bitbucket_server_provider_repo_entity.rb4
-rw-r--r--app/serializers/import/manifest_provider_repo_entity.rb23
-rw-r--r--app/serializers/import/provider_repo_serializer.rb2
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb16
-rw-r--r--app/serializers/merge_request_widget_entity.rb20
-rw-r--r--app/serializers/merge_requests/pipeline_entity.rb42
-rw-r--r--app/serializers/pipeline_entity.rb6
-rw-r--r--app/serializers/pipeline_serializer.rb1
-rw-r--r--app/serializers/prometheus_alert_entity.rb1
-rw-r--r--app/serializers/release_entity.rb6
-rw-r--r--app/serializers/release_serializer.rb5
-rw-r--r--app/serializers/suggestion_entity.rb18
-rw-r--r--app/serializers/test_report_summary_entity.rb4
-rw-r--r--app/serializers/triggered_pipeline_entity.rb4
-rw-r--r--app/services/admin/propagate_integration_service.rb6
-rw-r--r--app/services/alert_management/alerts/update_service.rb6
-rw-r--r--app/services/alert_management/create_alert_issue_service.rb32
-rw-r--r--app/services/award_emojis/copy_service.rb30
-rw-r--r--app/services/boards/issues/list_service.rb20
-rw-r--r--app/services/boards/issues/move_service.rb2
-rw-r--r--app/services/boards/lists/create_service.rb41
-rw-r--r--app/services/boards/lists/list_service.rb3
-rw-r--r--app/services/branches/create_service.rb5
-rw-r--r--app/services/ci/build_report_result_service.rb1
-rw-r--r--app/services/ci/change_variable_service.rb33
-rw-r--r--app/services/ci/change_variables_service.rb11
-rw-r--r--app/services/ci/create_cross_project_pipeline_service.rb2
-rw-r--r--app/services/ci/create_job_artifacts_service.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb53
-rw-r--r--app/services/ci/create_web_ide_terminal_service.rb2
-rw-r--r--app/services/ci/pipeline_processing/legacy_processing_service.rb122
-rw-r--r--app/services/ci/process_pipeline_service.rb14
-rw-r--r--app/services/ci/register_job_service.rb28
-rw-r--r--app/services/ci/retry_pipeline_service.rb8
-rw-r--r--app/services/clusters/aws/authorize_role_service.rb4
-rw-r--r--app/services/clusters/parse_cluster_applications_artifact_service.rb6
-rw-r--r--app/services/cohorts_service.rb2
-rw-r--r--app/services/commits/change_service.rb4
-rw-r--r--app/services/commits/create_service.rb3
-rw-r--r--app/services/concerns/incident_management/settings.rb3
-rw-r--r--app/services/design_management/move_designs_service.rb84
-rw-r--r--app/services/discussions/capture_diff_note_position_service.rb4
-rw-r--r--app/services/discussions/resolve_service.rb2
-rw-r--r--app/services/event_create_service.rb59
-rw-r--r--app/services/git/process_ref_changes_service.rb2
-rw-r--r--app/services/git/tag_push_service.rb4
-rw-r--r--app/services/git/wiki_push_service.rb7
-rw-r--r--app/services/git/wiki_push_service/change.rb4
-rw-r--r--app/services/groups/transfer_service.rb16
-rw-r--r--app/services/groups/update_service.rb16
-rw-r--r--app/services/import/github_service.rb2
-rw-r--r--app/services/incident_management/create_incident_label_service.rb20
-rw-r--r--app/services/incident_management/create_issue_service.rb93
-rw-r--r--app/services/incident_management/incidents/create_service.rb50
-rw-r--r--app/services/incident_management/pager_duty/create_incident_issue_service.rb34
-rw-r--r--app/services/incident_management/pager_duty/process_webhook_service.rb3
-rw-r--r--app/services/issuable/clone/base_service.rb32
-rw-r--r--app/services/issuable/clone/content_rewriter.rb74
-rw-r--r--app/services/issuable/common_system_notes_service.rb2
-rw-r--r--app/services/issues/build_service.rb2
-rw-r--r--app/services/issues/close_service.rb17
-rw-r--r--app/services/issues/reorder_service.rb2
-rw-r--r--app/services/issues/update_service.rb4
-rw-r--r--app/services/jira/requests/projects/list_service.rb6
-rw-r--r--app/services/jira_import/cloud_users_mapper_service.rb19
-rw-r--r--app/services/jira_import/server_users_mapper_service.rb19
-rw-r--r--app/services/jira_import/start_import_service.rb2
-rw-r--r--app/services/jira_import/users_importer.rb38
-rw-r--r--app/services/jira_import/users_mapper.rb30
-rw-r--r--app/services/jira_import/users_mapper_service.rb52
-rw-r--r--app/services/labels/available_labels_service.rb6
-rw-r--r--app/services/markdown_content_rewriter_service.rb32
-rw-r--r--app/services/merge_requests/after_create_service.rb2
-rw-r--r--app/services/merge_requests/conflicts/list_service.rb2
-rw-r--r--app/services/merge_requests/ff_merge_service.rb1
-rw-r--r--app/services/merge_requests/mergeability_check_service.rb2
-rw-r--r--app/services/merge_requests/pushed_branches_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/metrics/dashboard/base_service.rb4
-rw-r--r--app/services/metrics/dashboard/clone_dashboard_service.rb10
-rw-r--r--app/services/metrics/dashboard/cluster_dashboard_service.rb5
-rw-r--r--app/services/metrics/dashboard/custom_dashboard_service.rb13
-rw-r--r--app/services/metrics/dashboard/custom_metric_embed_service.rb1
-rw-r--r--app/services/metrics/dashboard/dynamic_embed_service.rb2
-rw-r--r--app/services/metrics/dashboard/gitlab_alert_embed_service.rb4
-rw-r--r--app/services/metrics/dashboard/grafana_metric_embed_service.rb4
-rw-r--r--app/services/metrics/dashboard/panel_preview_service.rb54
-rw-r--r--app/services/metrics/dashboard/pod_dashboard_service.rb22
-rw-r--r--app/services/metrics/dashboard/predefined_dashboard_service.rb8
-rw-r--r--app/services/metrics/dashboard/self_monitoring_dashboard_service.rb9
-rw-r--r--app/services/metrics/dashboard/system_dashboard_service.rb5
-rw-r--r--app/services/metrics/dashboard/update_dashboard_service.rb4
-rw-r--r--app/services/notes/copy_service.rb68
-rw-r--r--app/services/notes/create_service.rb7
-rw-r--r--app/services/notes/quick_actions_service.rb5
-rw-r--r--app/services/notes/update_service.rb2
-rw-r--r--app/services/notification_service.rb19
-rw-r--r--app/services/packages/maven/find_or_create_package_service.rb39
-rw-r--r--app/services/packages/nuget/metadata_extraction_service.rb2
-rw-r--r--app/services/packages/nuget/search_service.rb4
-rw-r--r--app/services/personal_access_tokens/revoke_service.rb36
-rw-r--r--app/services/product_analytics/build_graph_service.rb29
-rw-r--r--app/services/projects/alerting/notify_service.rb4
-rw-r--r--app/services/projects/auto_devops/disable_service.rb2
-rw-r--r--app/services/projects/cleanup_service.rb2
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb11
-rw-r--r--app/services/projects/container_repository/delete_tags_service.rb78
-rw-r--r--app/services/projects/container_repository/gitlab/delete_tags_service.rb29
-rw-r--r--app/services/projects/container_repository/third_party/delete_tags_service.rb64
-rw-r--r--app/services/projects/create_service.rb6
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/projects/fork_service.rb4
-rw-r--r--app/services/projects/prometheus/alerts/notify_service.rb6
-rw-r--r--app/services/projects/propagate_service_template.rb6
-rw-r--r--app/services/projects/transfer_service.rb8
-rw-r--r--app/services/projects/update_pages_configuration_service.rb17
-rw-r--r--app/services/projects/update_pages_service.rb2
-rw-r--r--app/services/projects/update_remote_mirror_service.rb6
-rw-r--r--app/services/projects/update_repository_storage_service.rb65
-rw-r--r--app/services/projects/update_service.rb8
-rw-r--r--app/services/resource_access_tokens/create_service.rb4
-rw-r--r--app/services/resource_events/base_change_timebox_service.rb35
-rw-r--r--app/services/resource_events/change_milestone_service.rb29
-rw-r--r--app/services/search_service.rb2
-rw-r--r--app/services/service_desk_settings/update_service.rb2
-rw-r--r--app/services/submit_usage_ping_service.rb44
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/system_notes/alert_management_service.rb20
-rw-r--r--app/services/system_notes/issuables_service.rb2
-rw-r--r--app/services/todo_service.rb14
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb17
-rw-r--r--app/services/web_hook_service.rb10
-rw-r--r--app/services/wiki_pages/base_service.rb8
-rw-r--r--app/services/wiki_pages/create_service.rb6
-rw-r--r--app/services/wiki_pages/destroy_service.rb4
-rw-r--r--app/services/wiki_pages/event_create_service.rb4
-rw-r--r--app/uploaders/ci/pipeline_artifact_uploader.rb21
-rw-r--r--app/uploaders/job_artifact_uploader.rb7
-rw-r--r--app/uploaders/object_storage.rb16
-rw-r--r--app/uploaders/packages/package_file_uploader.rb7
-rw-r--r--app/validators/html_safety_validator.rb2
-rw-r--r--app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json83
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml14
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml4
-rw-r--r--app/views/admin/application_settings/_diff_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_eks.html.haml2
-rw-r--r--app/views/admin/application_settings/_external_authorization_service_form.html.haml4
-rw-r--r--app/views/admin/application_settings/_initial_branch_name.html.haml4
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml8
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml3
-rw-r--r--app/views/admin/application_settings/_repository_static_objects.html.haml3
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml4
-rw-r--r--app/views/admin/application_settings/_signin.html.haml4
-rw-r--r--app/views/admin/application_settings/_signup.html.haml4
-rw-r--r--app/views/admin/application_settings/_snowplow.html.haml3
-rw-r--r--app/views/admin/application_settings/_sourcegraph.html.haml2
-rw-r--r--app/views/admin/application_settings/_terminal.html.haml4
-rw-r--r--app/views/admin/application_settings/_terms.html.haml4
-rw-r--r--app/views/admin/application_settings/_third_party_offers.html.haml3
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml4
-rw-r--r--app/views/admin/application_settings/ci/_header.html.haml6
-rw-r--r--app/views/admin/application_settings/general.html.haml21
-rw-r--r--app/views/admin/application_settings/integrations.html.haml40
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml2
-rw-r--r--app/views/admin/application_settings/network.html.haml4
-rw-r--r--app/views/admin/application_settings/preferences.html.haml2
-rw-r--r--app/views/admin/application_settings/repository.html.haml2
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml4
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml6
-rw-r--r--app/views/admin/dashboard/index.html.haml6
-rw-r--r--app/views/admin/dashboard/stats.html.haml2
-rw-r--r--app/views/admin/deploy_keys/index.html.haml11
-rw-r--r--app/views/admin/groups/_group.html.haml8
-rw-r--r--app/views/admin/groups/show.html.haml20
-rw-r--r--app/views/admin/health_check/show.html.haml2
-rw-r--r--app/views/admin/hook_logs/show.html.haml4
-rw-r--r--app/views/admin/labels/destroy.js.haml2
-rw-r--r--app/views/admin/projects/show.html.haml13
-rw-r--r--app/views/admin/requests_profiles/index.html.haml2
-rw-r--r--app/views/admin/runners/_runner.html.haml8
-rw-r--r--app/views/admin/runners/show.html.haml2
-rw-r--r--app/views/admin/services/index.html.haml2
-rw-r--r--app/views/admin/sessions/_signin_box.html.haml2
-rw-r--r--app/views/admin/users/_head.html.haml8
-rw-r--r--app/views/admin/users/_user_listing_note.html.haml2
-rw-r--r--app/views/ci/deploy_freeze/_index.html.haml2
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml4
-rw-r--r--app/views/ci/runner/_how_to_setup_runner_automatically.html.haml2
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml2
-rw-r--r--app/views/ci/variables/_content.html.haml9
-rw-r--r--app/views/ci/variables/_url_query_variable_row.html.haml4
-rw-r--r--app/views/ci/variables/_variable_row.html.haml4
-rw-r--r--app/views/clusters/clusters/_advanced_settings.html.haml10
-rw-r--r--app/views/clusters/clusters/_banner.html.haml8
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml3
-rw-r--r--app/views/clusters/clusters/_gitlab_integration_form.html.haml35
-rw-r--r--app/views/clusters/clusters/_provider_details_form.html.haml2
-rw-r--r--app/views/clusters/clusters/show.html.haml3
-rw-r--r--app/views/dashboard/_activities.html.haml4
-rw-r--r--app/views/dashboard/_projects_head.html.haml1
-rw-r--r--app/views/dashboard/projects/index.html.haml8
-rw-r--r--app/views/dashboard/projects/shared/_common.html.haml13
-rw-r--r--app/views/dashboard/projects/starred.html.haml15
-rw-r--r--app/views/devise/registrations/new.html.haml2
-rw-r--r--app/views/devise/sessions/_new_base.html.haml2
-rw-r--r--app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml5
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml2
-rw-r--r--app/views/doorkeeper/applications/_form.html.haml2
-rw-r--r--app/views/doorkeeper/applications/index.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml3
-rw-r--r--app/views/groups/_activities.html.haml5
-rw-r--r--app/views/groups/_flash_messages.html.haml1
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml115
-rw-r--r--app/views/groups/group_members/tab_pane/_form_item.html.haml2
-rw-r--r--app/views/groups/group_members/tab_pane/_header.html.haml2
-rw-r--r--app/views/groups/group_members/tab_pane/_title.html.haml2
-rw-r--r--app/views/groups/issues.html.haml4
-rw-r--r--app/views/groups/merge_requests.html.haml2
-rw-r--r--app/views/groups/packages/_legacy_package_list.haml59
-rw-r--r--app/views/groups/packages/index.html.haml5
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/groups/runners/_runner.html.haml6
-rw-r--r--app/views/groups/settings/_advanced.html.haml13
-rw-r--r--app/views/groups/settings/_export.html.haml5
-rw-r--r--app/views/groups/settings/_general.html.haml6
-rw-r--r--app/views/groups/settings/_pages_settings.html.haml2
-rw-r--r--app/views/groups/settings/_permanent_deletion.html.haml4
-rw-r--r--app/views/groups/settings/_permissions.html.haml5
-rw-r--r--app/views/groups/settings/ci_cd/_auto_devops_form.html.haml2
-rw-r--r--app/views/groups/sidebar/_packages.html.haml23
-rw-r--r--app/views/help/instance_configuration/_ssh_info.html.haml2
-rw-r--r--app/views/help/ui.html.haml524
-rw-r--r--app/views/import/_githubish_status.html.haml5
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml2
-rw-r--r--app/views/import/gitlab/status.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml2
-rw-r--r--app/views/import/google_code/new.html.haml20
-rw-r--r--app/views/import/google_code/new_user_map.html.haml10
-rw-r--r--app/views/import/google_code/status.html.haml2
-rw-r--r--app/views/import/manifest/_form.html.haml2
-rw-r--r--app/views/import/manifest/status.html.haml37
-rw-r--r--app/views/import/phabricator/new.html.haml6
-rw-r--r--app/views/instance_statistics/dev_ops_score/_card.html.haml8
-rw-r--r--app/views/invites/show.html.haml29
-rw-r--r--app/views/layouts/_flash.html.haml7
-rw-r--r--app/views/layouts/_head.html.haml12
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/_search.html.haml6
-rw-r--r--app/views/layouts/_startup_css.haml4
-rw-r--r--app/views/layouts/_startup_css_activation.haml7
-rw-r--r--app/views/layouts/devise_experimental_onboarding_issues.html.haml11
-rw-r--r--app/views/layouts/group.html.haml1
-rw-r--r--app/views/layouts/header/_default.html.haml44
-rw-r--r--app/views/layouts/header/_new_dropdown.haml2
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml4
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml25
-rw-r--r--app/views/layouts/nav/sidebar/_project_packages_link.html.haml23
-rw-r--r--app/views/layouts/project.html.haml1
-rw-r--r--app/views/notify/_relabeled_issuable_email.text.erb2
-rw-r--r--app/views/notify/access_token_about_to_expire_email.html.haml2
-rw-r--r--app/views/notify/access_token_about_to_expire_email.text.erb2
-rw-r--r--app/views/notify/access_token_expired_email.html.haml7
-rw-r--r--app/views/notify/access_token_expired_email.text.erb5
-rw-r--r--app/views/notify/reassigned_issue_email.text.erb2
-rw-r--r--app/views/notify/reassigned_merge_request_email.text.erb2
-rw-r--r--app/views/notify/service_desk_new_note_email.html.haml2
-rw-r--r--app/views/notify/service_desk_new_note_email.text.erb4
-rw-r--r--app/views/notify/service_desk_thank_you_email.html.haml2
-rw-r--r--app/views/notify/service_desk_thank_you_email.text.erb4
-rw-r--r--app/views/profiles/accounts/show.html.haml4
-rw-r--r--app/views/profiles/chat_names/new.html.haml11
-rw-r--r--app/views/profiles/emails/index.html.haml2
-rw-r--r--app/views/profiles/gpg_keys/_key.html.haml2
-rw-r--r--app/views/profiles/keys/_key.html.haml8
-rw-r--r--app/views/profiles/preferences/_sourcegraph.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml12
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/profiles/two_factor_auths/_codes.html.haml4
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml4
-rw-r--r--app/views/projects/_activity.html.haml13
-rw-r--r--app/views/projects/_export.html.haml3
-rw-r--r--app/views/projects/_flash_messages.html.haml1
-rw-r--r--app/views/projects/_home_panel.html.haml4
-rw-r--r--app/views/projects/_import_project_pane.html.haml7
-rw-r--r--app/views/projects/_merge_request_settings.html.haml2
-rw-r--r--app/views/projects/_remove.html.haml9
-rw-r--r--app/views/projects/_service_desk_settings.html.haml3
-rw-r--r--app/views/projects/_visibility_modal.html.haml11
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml7
-rw-r--r--app/views/projects/blob/_header_content.html.haml2
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml3
-rw-r--r--app/views/projects/blob/new.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_changelog.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_contributing.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_dependency_manager.html.haml7
-rw-r--r--app/views/projects/blob/viewers/_license.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_readme.html.haml2
-rw-r--r--app/views/projects/branches/_panel.html.haml2
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml6
-rw-r--r--app/views/projects/ci/builds/_build.html.haml6
-rw-r--r--app/views/projects/ci/lints/_create.html.haml77
-rw-r--r--app/views/projects/ci/lints/_lint_warnings.html.haml6
-rw-r--r--app/views/projects/ci/lints/show.html.haml8
-rw-r--r--app/views/projects/cleanup/_show.html.haml8
-rw-r--r--app/views/projects/commit/_change.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/commit/_same_user_different_email_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/_unverified_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/diff_files.html.haml3
-rw-r--r--app/views/projects/commit/x509/_verified_signature_badge.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml7
-rw-r--r--app/views/projects/commits/_commits.html.haml19
-rw-r--r--app/views/projects/commits/show.html.haml4
-rw-r--r--app/views/projects/compare/_form.html.haml4
-rw-r--r--app/views/projects/compare/index.html.haml4
-rw-r--r--app/views/projects/default_branch/_show.html.haml5
-rw-r--r--app/views/projects/deploy_keys/edit.html.haml2
-rw-r--r--app/views/projects/deployments/_actions.haml2
-rw-r--r--app/views/projects/deployments/_confirm_rollback_modal.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml10
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml2
-rw-r--r--app/views/projects/diffs/_warning.html.haml2
-rw-r--r--app/views/projects/edit.html.haml31
-rw-r--r--app/views/projects/environments/_form.html.haml2
-rw-r--r--app/views/projects/environments/show.html.haml2
-rw-r--r--app/views/projects/forks/_fork_button.html.haml40
-rw-r--r--app/views/projects/forks/error.html.haml2
-rw-r--r--app/views/projects/forks/new.html.haml15
-rw-r--r--app/views/projects/hooks/edit.html.haml2
-rw-r--r--app/views/projects/hooks/index.html.haml5
-rw-r--r--app/views/projects/incidents/index.html.haml3
-rw-r--r--app/views/projects/issues/_alert_moved_from_service_desk.html.haml4
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
-rw-r--r--app/views/projects/issues/_form.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml6
-rw-r--r--app/views/projects/issues/_issue_estimate.html.haml2
-rw-r--r--app/views/projects/issues/_issues.html.haml11
-rw-r--r--app/views/projects/issues/_service_desk_empty_state.html.haml33
-rw-r--r--app/views/projects/issues/_service_desk_info_content.html.haml44
-rw-r--r--app/views/projects/issues/export_csv/_modal.html.haml4
-rw-r--r--app/views/projects/issues/service_desk.html.haml18
-rw-r--r--app/views/projects/issues/show.html.haml8
-rw-r--r--app/views/projects/issues/verify.html.haml4
-rw-r--r--app/views/projects/labels/index.html.haml4
-rw-r--r--app/views/projects/merge_requests/_approvals_count.html.haml2
-rw-r--r--app/views/projects/merge_requests/_awards_block.html.haml7
-rw-r--r--app/views/projects/merge_requests/_commits.html.haml18
-rw-r--r--app/views/projects/merge_requests/_form.html.haml2
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml4
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml6
-rw-r--r--app/views/projects/merge_requests/_mr_box.html.haml7
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml7
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml3
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/creations/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/diffs/_commit_widget.html.haml6
-rw-r--r--app/views/projects/merge_requests/show.html.haml31
-rw-r--r--app/views/projects/milestones/_form.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml8
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml11
-rw-r--r--app/views/projects/mirrors/_mirror_repos_push.html.haml2
-rw-r--r--app/views/projects/mirrors/_ssh_host_keys.html.haml2
-rw-r--r--app/views/projects/no_repo.html.haml2
-rw-r--r--app/views/projects/notes/_actions.html.haml2
-rw-r--r--app/views/projects/packages/packages/_legacy_package_list.html.haml60
-rw-r--r--app/views/projects/packages/packages/index.html.haml5
-rw-r--r--app/views/projects/packages/packages/show.html.haml25
-rw-r--r--app/views/projects/pages/_access.html.haml2
-rw-r--r--app/views/projects/pages/_list.html.haml2
-rw-r--r--app/views/projects/pages/_pages_settings.html.haml4
-rw-r--r--app/views/projects/pages/show.html.haml2
-rw-r--r--app/views/projects/pages_domains/_certificate.html.haml4
-rw-r--r--app/views/projects/pages_domains/new.html.haml2
-rw-r--r--app/views/projects/pages_domains/show.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml4
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml6
-rw-r--r--app/views/projects/pipelines/_info.html.haml6
-rw-r--r--app/views/projects/pipelines/_pipeline_warnings.html.haml6
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml23
-rw-r--r--app/views/projects/pipelines/new.html.haml67
-rw-r--r--app/views/projects/pipelines/show.html.haml8
-rw-r--r--app/views/projects/product_analytics/_graph.html.haml6
-rw-r--r--app/views/projects/product_analytics/_links.html.haml10
-rw-r--r--app/views/projects/product_analytics/_tracker.html.erb10
-rw-r--r--app/views/projects/product_analytics/graphs.html.haml12
-rw-r--r--app/views/projects/product_analytics/index.html.haml16
-rw-r--r--app/views/projects/product_analytics/setup.html.haml12
-rw-r--r--app/views/projects/product_analytics/test.html.haml16
-rw-r--r--app/views/projects/project_members/_groups.html.haml2
-rw-r--r--app/views/projects/project_members/_team.html.haml3
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/protected_branches/_update_protected_branch.html.haml11
-rw-r--r--app/views/projects/protected_branches/shared/_create_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_index.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_tags/shared/_create_protected_tag.html.haml6
-rw-r--r--app/views/projects/protected_tags/shared/_protected_tag.html.haml2
-rw-r--r--app/views/projects/runners/_runner.html.haml9
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml2
-rw-r--r--app/views/projects/serverless/functions/index.html.haml5
-rw-r--r--app/views/projects/services/_form.html.haml2
-rw-r--r--app/views/projects/services/alerts/_top.html.haml4
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml10
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_metrics.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_top.html.haml4
-rw-r--r--app/views/projects/services/slack/_help.haml10
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml14
-rw-r--r--app/views/projects/settings/_archive.html.haml5
-rw-r--r--app/views/projects/settings/_general.html.haml8
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml5
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml19
-rw-r--r--app/views/projects/settings/integrations/show.html.haml4
-rw-r--r--app/views/projects/settings/operations/_error_tracking.html.haml4
-rw-r--r--app/views/projects/settings/operations/_metrics_dashboard.html.haml4
-rw-r--r--app/views/projects/snippets/verify.html.haml4
-rw-r--r--app/views/projects/starrers/index.html.haml2
-rw-r--r--app/views/projects/static_site_editor/show.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml4
-rw-r--r--app/views/projects/tags/show.html.haml16
-rw-r--r--app/views/projects/triggers/_form.html.haml2
-rw-r--r--app/views/registrations/welcome.html.haml2
-rw-r--r--app/views/search/_form.html.haml2
-rw-r--r--app/views/search/_results.html.haml4
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_milestone.html.haml2
-rw-r--r--app/views/search/results/_note.html.haml2
-rw-r--r--app/views/search/show.html.haml4
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml2
-rw-r--r--app/views/shared/_broadcast_message.html.haml2
-rw-r--r--app/views/shared/_check_recovery_settings.html.haml2
-rw-r--r--app/views/shared/_confirm_fork_modal.html.haml4
-rw-r--r--app/views/shared/_confirm_modal.html.haml2
-rw-r--r--app/views/shared/_delete_label_modal.html.haml2
-rw-r--r--app/views/shared/_field.html.haml29
-rw-r--r--app/views/shared/_file_highlight.html.haml2
-rw-r--r--app/views/shared/_group_form.html.haml6
-rw-r--r--app/views/shared/_import_form.html.haml4
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml8
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_md_preview.html.haml2
-rw-r--r--app/views/shared/_merge_requests.html.haml2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml6
-rw-r--r--app/views/shared/_no_ssh.html.haml4
-rw-r--r--app/views/shared/_outdated_browser.html.haml2
-rw-r--r--app/views/shared/_service_settings.html.haml25
-rw-r--r--app/views/shared/_sidebar_toggle_button.html.haml2
-rw-r--r--app/views/shared/_zen.html.haml4
-rw-r--r--app/views/shared/access_tokens/_table.html.haml2
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/boards/components/sidebar/_due_date.html.haml6
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml6
-rw-r--r--app/views/shared/boards/components/sidebar/_milestone.html.haml6
-rw-r--r--app/views/shared/buttons/_project_feature_toggle.html.haml4
-rw-r--r--app/views/shared/deploy_keys/_index.html.haml2
-rw-r--r--app/views/shared/deploy_keys/_project_group_form.html.haml6
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml2
-rw-r--r--app/views/shared/deploy_tokens/_index.html.haml2
-rw-r--r--app/views/shared/deploy_tokens/_table.html.haml2
-rw-r--r--app/views/shared/empty_states/_deploy_keys.html.haml9
-rw-r--r--app/views/shared/empty_states/_issues.html.haml14
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml4
-rw-r--r--app/views/shared/groups/_group.html.haml8
-rw-r--r--app/views/shared/integrations/_form.html.haml25
-rw-r--r--app/views/shared/issuable/_assignees.html.haml4
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml27
-rw-r--r--app/views/shared/issuable/_feed_buttons.html.haml4
-rw-r--r--app/views/shared/issuable/_form.html.haml13
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml6
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml2
-rw-r--r--app/views/shared/issuable/form/_title.html.haml24
-rw-r--r--app/views/shared/members/_filter_2fa_dropdown.html.haml2
-rw-r--r--app/views/shared/members/_group.html.haml2
-rw-r--r--app/views/shared/members/_invite_group.html.haml2
-rw-r--r--app/views/shared/members/_invite_member.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml15
-rw-r--r--app/views/shared/members/_search_field.html.haml6
-rw-r--r--app/views/shared/members/_sort_dropdown.html.haml1
-rw-r--r--app/views/shared/milestones/_deprecation_message.html.haml15
-rw-r--r--app/views/shared/milestones/_milestone.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml4
-rw-r--r--app/views/shared/notes/_hints.html.haml6
-rw-r--r--app/views/shared/notes/_note.html.haml3
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/shared/notifications/_button.html.haml8
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml1
-rw-r--r--app/views/shared/packages/_no_packages.html.haml7
-rw-r--r--app/views/shared/projects/_archived.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml8
-rw-r--r--app/views/shared/projects/_search_bar.html.haml3
-rw-r--r--app/views/shared/projects/protected_branches/_update_protected_branch.html.haml35
-rw-r--r--app/views/shared/promotions/_promote_servicedesk.html.haml8
-rw-r--r--app/views/shared/snippets/_embed.html.haml10
-rw-r--r--app/views/shared/snippets/_form.html.haml3
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml6
-rw-r--r--app/views/shared/snippets/show.js.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml6
-rw-r--r--app/views/shared/wikis/_form.html.haml2
-rw-r--r--app/views/shared/wikis/_sidebar.html.haml4
-rw-r--r--app/views/shared/wikis/_wiki_directory.html.haml2
-rw-r--r--app/views/snippets/verify.html.haml4
-rw-r--r--app/views/u2f/_register.html.haml8
-rw-r--r--app/views/users/_deletion_guidance.html.haml2
-rw-r--r--app/views/users/calendar_activities.html.haml6
-rw-r--r--app/views/users/show.html.haml34
-rw-r--r--app/views/users/terms/index.html.haml2
-rw-r--r--app/workers/admin_email_worker.rb2
-rw-r--r--app/workers/all_queues.yml790
-rw-r--r--app/workers/concerns/application_worker.rb1
-rw-r--r--app/workers/flush_counter_increments_worker.rb26
-rw-r--r--app/workers/git_garbage_collect_worker.rb18
-rw-r--r--app/workers/gitlab/import/advance_stage.rb2
-rw-r--r--app/workers/gitlab_usage_ping_worker.rb28
-rw-r--r--app/workers/incident_management/process_alert_worker.rb27
-rw-r--r--app/workers/pages_update_configuration_worker.rb23
-rw-r--r--app/workers/pages_worker.rb8
-rw-r--r--app/workers/partition_creation_worker.rb2
-rw-r--r--app/workers/personal_access_tokens/expired_notification_worker.rb26
-rw-r--r--app/workers/pipeline_process_worker.rb6
-rw-r--r--app/workers/pipeline_update_worker.rb6
-rw-r--r--app/workers/propagate_service_template_worker.rb2
1855 files changed, 23844 insertions, 10272 deletions
diff --git a/app/assets/images/mailers/approval/icon-merge-request-gray.gif b/app/assets/images/mailers/approval/icon-merge-request-gray.gif
new file mode 100644
index 00000000000..6eef39d3b1e
--- /dev/null
+++ b/app/assets/images/mailers/approval/icon-merge-request-gray.gif
Binary files differ
diff --git a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_trigger.vue b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_trigger.vue
new file mode 100644
index 00000000000..78a575ffe96
--- /dev/null
+++ b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_trigger.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import eventHub from '../event_hub';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ commitsEmpty: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ contextCommitsEmpty: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ buttonText() {
+ return this.contextCommitsEmpty || this.commitsEmpty
+ ? s__('AddContextCommits|Add previously merged commits')
+ : s__('AddContextCommits|Add/remove');
+ },
+ },
+ methods: {
+ openModal() {
+ eventHub.$emit('openModal');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button
+ :class="[
+ {
+ 'ml-3': !contextCommitsEmpty,
+ 'mt-3': !commitsEmpty && contextCommitsEmpty,
+ },
+ ]"
+ :variant="commitsEmpty ? 'info' : 'default'"
+ @click="openModal"
+ >
+ {{ buttonText }}
+ </gl-button>
+</template>
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
new file mode 100644
index 00000000000..cb9aa50fa68
--- /dev/null
+++ b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
@@ -0,0 +1,279 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlModal, GlTabs, GlTab, GlSearchBoxByType, GlSprintf } from '@gitlab/ui';
+import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
+import { s__ } from '~/locale';
+import eventHub from '../event_hub';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import {
+ findCommitIndex,
+ setCommitStatus,
+ removeIfReadyToBeRemoved,
+ removeIfPresent,
+} from '../utils';
+
+export default {
+ components: {
+ GlModal,
+ GlTabs,
+ GlTab,
+ ReviewTabContainer,
+ GlSearchBoxByType,
+ GlSprintf,
+ },
+ props: {
+ contextCommitsPath: {
+ type: String,
+ required: true,
+ },
+ targetBranch: {
+ type: String,
+ required: true,
+ },
+ mergeRequestIid: {
+ type: Number,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'tabIndex',
+ 'isLoadingCommits',
+ 'commits',
+ 'commitsLoadingError',
+ 'isLoadingContextCommits',
+ 'contextCommits',
+ 'contextCommitsLoadingError',
+ 'selectedCommits',
+ 'searchText',
+ 'toRemoveCommits',
+ ]),
+ currentTabIndex: {
+ get() {
+ return this.tabIndex;
+ },
+ set(newTabIndex) {
+ this.setTabIndex(newTabIndex);
+ },
+ },
+ selectedCommitsCount() {
+ return this.selectedCommits.filter(selectedCommit => selectedCommit.isSelected).length;
+ },
+ shouldPurge() {
+ return this.selectedCommitsCount !== this.selectedCommits.length;
+ },
+ uniqueCommits() {
+ return this.selectedCommits.filter(
+ selectedCommit =>
+ selectedCommit.isSelected &&
+ findCommitIndex(this.contextCommits, selectedCommit.short_id) === -1,
+ );
+ },
+ disableSaveButton() {
+ // We should have a minimum of one commit selected and that should not be in the context commits list or we should have a context commit to delete
+ return (
+ (this.selectedCommitsCount.length === 0 || this.uniqueCommits.length === 0) &&
+ this.toRemoveCommits.length === 0
+ );
+ },
+ },
+ watch: {
+ tabIndex(newTabIndex) {
+ this.handleTabChange(newTabIndex);
+ },
+ },
+ mounted() {
+ eventHub.$on('openModal', this.openModal);
+ this.setBaseConfig({
+ contextCommitsPath: this.contextCommitsPath,
+ mergeRequestIid: this.mergeRequestIid,
+ projectId: this.projectId,
+ });
+ },
+ beforeDestroy() {
+ eventHub.$off('openModal', this.openModal);
+ clearTimeout(this.timeout);
+ this.timeout = null;
+ },
+ methods: {
+ ...mapActions([
+ 'setBaseConfig',
+ 'setTabIndex',
+ 'searchCommits',
+ 'setCommits',
+ 'createContextCommits',
+ 'fetchContextCommits',
+ 'removeContextCommits',
+ 'setSelectedCommits',
+ 'setSearchText',
+ 'setToRemoveCommits',
+ 'resetModalState',
+ ]),
+ focusSearch() {
+ this.$refs.searchInput.focusInput();
+ },
+ openModal() {
+ this.searchCommits();
+ this.fetchContextCommits();
+ this.$root.$emit('bv::show::modal', 'add-review-item');
+ },
+ handleTabChange(tabIndex) {
+ if (tabIndex === 0) {
+ this.focusSearch();
+ if (this.shouldPurge) {
+ this.setSelectedCommits(
+ [...this.commits, ...this.selectedCommits].filter(commit => commit.isSelected),
+ );
+ }
+ }
+ },
+ handleSearchCommits(value) {
+ // We only call the service, if we have 3 characters or we don't have any characters
+ if (value.length >= 3) {
+ clearTimeout(this.timeout);
+ this.timeout = setTimeout(() => {
+ this.searchCommits(value);
+ }, 500);
+ } else if (value.length === 0) {
+ this.searchCommits();
+ }
+ this.setSearchText(value);
+ },
+ handleCommitRowSelect(event) {
+ const index = event[0];
+ const selected = event[1];
+ const tempCommit = this.tabIndex === 0 ? this.commits[index] : this.selectedCommits[index];
+ const commitIndex = findCommitIndex(this.commits, tempCommit.short_id);
+ const tempCommits = setCommitStatus(this.commits, commitIndex, selected);
+ const selectedCommitIndex = findCommitIndex(this.selectedCommits, tempCommit.short_id);
+ let tempSelectedCommits = setCommitStatus(
+ this.selectedCommits,
+ selectedCommitIndex,
+ selected,
+ );
+
+ if (selected) {
+ // If user deselects a commit which is already present in previously merged commits, then user adds it again.
+ // Then the state is neutral, so we remove it from the list
+ this.setToRemoveCommits(
+ removeIfReadyToBeRemoved(this.toRemoveCommits, tempCommit.short_id),
+ );
+ } else {
+ // If user is present in first tab and deselects a commit, remove it directly
+ if (this.tabIndex === 0) {
+ tempSelectedCommits = removeIfPresent(tempSelectedCommits, tempCommit.short_id);
+ }
+
+ // If user deselects a commit which is already present in previously merged commits, we keep track of it in a list to remove
+ const contextCommitsIndex = findCommitIndex(this.contextCommits, tempCommit.short_id);
+ if (contextCommitsIndex !== -1) {
+ this.setToRemoveCommits([...this.toRemoveCommits, tempCommit.short_id]);
+ }
+ }
+
+ this.setCommits({ commits: tempCommits });
+ this.setSelectedCommits([
+ ...tempSelectedCommits,
+ ...tempCommits.filter(commit => commit.isSelected),
+ ]);
+ },
+ handleCreateContextCommits() {
+ if (this.uniqueCommits.length > 0 && this.toRemoveCommits.length > 0) {
+ return Promise.all([
+ this.createContextCommits({ commits: this.uniqueCommits }),
+ this.removeContextCommits(),
+ ]).then(values => {
+ if (values[0] || values[1]) {
+ window.location.reload();
+ }
+ if (!values[0] && !values[1]) {
+ createFlash(
+ s__('ContextCommits|Failed to create/remove context commits. Please try again.'),
+ );
+ }
+ });
+ } else if (this.uniqueCommits.length > 0) {
+ return this.createContextCommits({ commits: this.uniqueCommits, forceReload: true });
+ }
+
+ return this.removeContextCommits(true);
+ },
+ handleModalClose() {
+ this.resetModalState();
+ clearTimeout(this.timeout);
+ },
+ handleModalHide() {
+ this.resetModalState();
+ clearTimeout(this.timeout);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ cancel-variant="light"
+ size="md"
+ body-class="add-review-item pt-0"
+ :scrollable="true"
+ :ok-title="__('Save changes')"
+ modal-id="add-review-item"
+ :title="__('Add or remove previously merged commits')"
+ :ok-disabled="disableSaveButton"
+ @shown="focusSearch"
+ @ok="handleCreateContextCommits"
+ @cancel="handleModalClose"
+ @close="handleModalClose"
+ @hide="handleModalHide"
+ >
+ <gl-tabs v-model="currentTabIndex" content-class="pt-0">
+ <gl-tab>
+ <template #title>
+ <gl-sprintf :message="__(`Commits in %{codeStart}${targetBranch}%{codeEnd}`)">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </template>
+ <div class="mt-2">
+ <gl-search-box-by-type
+ ref="searchInput"
+ :placeholder="__(`Search by commit title or SHA`)"
+ @input="handleSearchCommits"
+ />
+ <review-tab-container
+ :is-loading="isLoadingCommits"
+ :loading-error="commitsLoadingError"
+ :loading-failed-text="__('Unable to load commits. Try again later.')"
+ :commits="commits"
+ :empty-list-text="__('Your search didn\'t match any commits. Try a different query.')"
+ @handleCommitSelect="handleCommitRowSelect"
+ />
+ </div>
+ </gl-tab>
+ <gl-tab>
+ <template #title>
+ {{ __('Selected commits') }}
+ <span class="badge badge-pill">{{ selectedCommitsCount }}</span>
+ </template>
+ <review-tab-container
+ :is-loading="isLoadingContextCommits"
+ :loading-error="contextCommitsLoadingError"
+ :loading-failed-text="__('Unable to load commits. Try again later.')"
+ :commits="selectedCommits"
+ :empty-list-text="
+ __(
+ 'Commits you select appear here. Go to the first tab and select commits to add to this merge request.',
+ )
+ "
+ @handleCommitSelect="handleCommitRowSelect"
+ />
+ </gl-tab>
+ </gl-tabs>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/add_context_commits_modal/components/review_tab_container.vue b/app/assets/javascripts/add_context_commits_modal/components/review_tab_container.vue
new file mode 100644
index 00000000000..36e3449ff27
--- /dev/null
+++ b/app/assets/javascripts/add_context_commits_modal/components/review_tab_container.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import CommitItem from '~/diffs/components/commit_item.vue';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlAlert,
+ CommitItem,
+ },
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ loadingError: {
+ type: Boolean,
+ required: true,
+ },
+ loadingFailedText: {
+ type: String,
+ required: true,
+ },
+ commits: {
+ type: Array,
+ required: true,
+ },
+ emptyListText: {
+ type: String,
+ required: false,
+ default: __('No commits present here'),
+ },
+ },
+};
+</script>
+<template>
+ <gl-loading-icon v-if="isLoading" size="lg" class="mt-3" />
+ <gl-alert v-else-if="loadingError" variant="danger" :dismissible="false" class="mt-3">
+ {{ loadingFailedText }}
+ </gl-alert>
+ <div v-else-if="commits.length === 0" class="text-center mt-4">
+ <span>{{ emptyListText }}</span>
+ </div>
+ <div v-else>
+ <ul class="content-list commit-list flex-list">
+ <commit-item
+ v-for="(commit, index) in commits"
+ :key="commit.id"
+ :is-selectable="true"
+ :commit="commit"
+ :checked="commit.isSelected"
+ @handleCheckboxChange="$emit('handleCommitSelect', [index, $event])"
+ />
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/import_projects/event_hub.js b/app/assets/javascripts/add_context_commits_modal/event_hub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/import_projects/event_hub.js
+++ b/app/assets/javascripts/add_context_commits_modal/event_hub.js
diff --git a/app/assets/javascripts/add_context_commits_modal/index.js b/app/assets/javascripts/add_context_commits_modal/index.js
new file mode 100644
index 00000000000..b5cd111fabc
--- /dev/null
+++ b/app/assets/javascripts/add_context_commits_modal/index.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import createStore from './store';
+import AddContextCommitsModalTrigger from './components/add_context_commits_modal_trigger.vue';
+import AddContextCommitsModalWrapper from './components/add_context_commits_modal_wrapper.vue';
+
+export default function initAddContextCommitsTriggers() {
+ const addContextCommitsModalTriggerEl = document.querySelector('.add-review-item-modal-trigger');
+ const addContextCommitsModalWrapperEl = document.querySelector('.add-review-item-modal-wrapper');
+
+ if (addContextCommitsModalTriggerEl || addContextCommitsModalWrapperEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: addContextCommitsModalTriggerEl,
+ data() {
+ const { commitsEmpty, contextCommitsEmpty } = this.$options.el.dataset;
+ return {
+ commitsEmpty: parseBoolean(commitsEmpty),
+ contextCommitsEmpty: parseBoolean(contextCommitsEmpty),
+ };
+ },
+ render(createElement) {
+ return createElement(AddContextCommitsModalTrigger, {
+ props: {
+ commitsEmpty: this.commitsEmpty,
+ contextCommitsEmpty: this.contextCommitsEmpty,
+ },
+ });
+ },
+ });
+
+ const store = createStore();
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: addContextCommitsModalWrapperEl,
+ store,
+ data() {
+ const {
+ contextCommitsPath,
+ targetBranch,
+ mergeRequestIid,
+ projectId,
+ } = this.$options.el.dataset;
+ return {
+ contextCommitsPath,
+ targetBranch,
+ mergeRequestIid: Number(mergeRequestIid),
+ projectId: Number(projectId),
+ };
+ },
+ render(createElement) {
+ return createElement(AddContextCommitsModalWrapper, {
+ props: {
+ contextCommitsPath: this.contextCommitsPath,
+ targetBranch: this.targetBranch,
+ mergeRequestIid: this.mergeRequestIid,
+ projectId: this.projectId,
+ },
+ });
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js
new file mode 100644
index 00000000000..d23955182b2
--- /dev/null
+++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js
@@ -0,0 +1,134 @@
+import _ from 'lodash';
+import axios from '~/lib/utils/axios_utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { s__ } from '~/locale';
+import Api from '~/api';
+import * as types from './mutation_types';
+
+export const setBaseConfig = ({ commit }, options) => {
+ commit(types.SET_BASE_CONFIG, options);
+};
+
+export const setTabIndex = ({ commit }, tabIndex) => commit(types.SET_TABINDEX, tabIndex);
+
+export const searchCommits = ({ dispatch, commit, state }, searchText) => {
+ commit(types.FETCH_COMMITS);
+
+ let params = {};
+ if (searchText) {
+ params = {
+ params: {
+ search: searchText,
+ per_page: 40,
+ },
+ };
+ }
+
+ return axios
+ .get(state.contextCommitsPath, params)
+ .then(({ data }) => {
+ let commits = data.map(o => ({ ...o, isSelected: false }));
+ commits = commits.map(c => {
+ const isPresent = state.selectedCommits.find(
+ selectedCommit => selectedCommit.short_id === c.short_id && selectedCommit.isSelected,
+ );
+ if (isPresent) {
+ return { ...c, isSelected: true };
+ }
+ return c;
+ });
+ if (!searchText) {
+ dispatch('setCommits', { commits: [...commits, ...state.contextCommits] });
+ } else {
+ dispatch('setCommits', { commits });
+ }
+ })
+ .catch(() => {
+ commit(types.FETCH_COMMITS_ERROR);
+ });
+};
+
+export const setCommits = ({ commit }, { commits: data, silentAddition = false }) => {
+ let commits = _.uniqBy(data, 'short_id');
+ commits = _.orderBy(data, c => new Date(c.committed_date), ['desc']);
+ if (silentAddition) {
+ commit(types.SET_COMMITS_SILENT, commits);
+ } else {
+ commit(types.SET_COMMITS, commits);
+ }
+};
+
+export const createContextCommits = ({ state }, { commits, forceReload = false }) =>
+ Api.createContextCommits(state.projectId, state.mergeRequestIid, {
+ commits: commits.map(commit => commit.short_id),
+ })
+ .then(() => {
+ if (forceReload) {
+ window.location.reload();
+ }
+
+ return true;
+ })
+ .catch(() => {
+ if (forceReload) {
+ createFlash(s__('ContextCommits|Failed to create context commits. Please try again.'));
+ }
+
+ return false;
+ });
+
+export const fetchContextCommits = ({ dispatch, commit, state }) => {
+ commit(types.FETCH_CONTEXT_COMMITS);
+ return Api.allContextCommits(state.projectId, state.mergeRequestIid)
+ .then(({ data }) => {
+ const contextCommits = data.map(o => ({ ...o, isSelected: true }));
+ dispatch('setContextCommits', contextCommits);
+ dispatch('setCommits', {
+ commits: [...state.commits, ...contextCommits],
+ silentAddition: true,
+ });
+ dispatch('setSelectedCommits', contextCommits);
+ })
+ .catch(() => {
+ commit(types.FETCH_CONTEXT_COMMITS_ERROR);
+ });
+};
+
+export const setContextCommits = ({ commit }, data) => {
+ commit(types.SET_CONTEXT_COMMITS, data);
+};
+
+export const removeContextCommits = ({ state }, forceReload = false) =>
+ Api.removeContextCommits(state.projectId, state.mergeRequestIid, {
+ commits: state.toRemoveCommits,
+ })
+ .then(() => {
+ if (forceReload) {
+ window.location.reload();
+ }
+
+ return true;
+ })
+ .catch(() => {
+ if (forceReload) {
+ createFlash(s__('ContextCommits|Failed to delete context commits. Please try again.'));
+ }
+
+ return false;
+ });
+
+export const setSelectedCommits = ({ commit }, selected) => {
+ let selectedCommits = _.uniqBy(selected, 'short_id');
+ selectedCommits = _.orderBy(
+ selectedCommits,
+ selectedCommit => new Date(selectedCommit.committed_date),
+ ['desc'],
+ );
+ commit(types.SET_SELECTED_COMMITS, selectedCommits);
+};
+
+export const setSearchText = ({ commit }, searchText) => commit(types.SET_SEARCH_TEXT, searchText);
+
+export const setToRemoveCommits = ({ commit }, data) => commit(types.SET_TO_REMOVE_COMMITS, data);
+
+export const resetModalState = ({ commit }) => commit(types.RESET_MODAL_STATE);
diff --git a/app/assets/javascripts/add_context_commits_modal/store/index.js b/app/assets/javascripts/add_context_commits_modal/store/index.js
new file mode 100644
index 00000000000..0bf3441379b
--- /dev/null
+++ b/app/assets/javascripts/add_context_commits_modal/store/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ namespaced: true,
+ state: state(),
+ actions,
+ mutations,
+ });
diff --git a/app/assets/javascripts/add_context_commits_modal/store/mutation_types.js b/app/assets/javascripts/add_context_commits_modal/store/mutation_types.js
new file mode 100644
index 00000000000..eda82f3984d
--- /dev/null
+++ b/app/assets/javascripts/add_context_commits_modal/store/mutation_types.js
@@ -0,0 +1,20 @@
+export const SET_BASE_CONFIG = 'SET_BASE_CONFIG';
+
+export const SET_TABINDEX = 'SET_TABINDEX';
+
+export const FETCH_COMMITS = 'FETCH_COMMITS';
+export const SET_COMMITS = 'SET_COMMITS';
+export const SET_COMMITS_SILENT = 'SET_COMMITS_SILENT';
+export const FETCH_COMMITS_ERROR = 'FETCH_COMMITS_ERROR';
+
+export const FETCH_CONTEXT_COMMITS = 'FETCH_CONTEXT_COMMITS';
+export const SET_CONTEXT_COMMITS = 'SET_CONTEXT_COMMITS';
+export const FETCH_CONTEXT_COMMITS_ERROR = 'FETCH_CONTEXT_COMMITS_ERROR';
+
+export const SET_SELECTED_COMMITS = 'SET_SELECTED_COMMITS';
+
+export const SET_SEARCH_TEXT = 'SET_SEARCH_TEXT';
+
+export const SET_TO_REMOVE_COMMITS = 'SET_TO_REMOVE_COMMITS';
+
+export const RESET_MODAL_STATE = 'RESET_MODAL_STATE';
diff --git a/app/assets/javascripts/add_context_commits_modal/store/mutations.js b/app/assets/javascripts/add_context_commits_modal/store/mutations.js
new file mode 100644
index 00000000000..8a3da0ca248
--- /dev/null
+++ b/app/assets/javascripts/add_context_commits_modal/store/mutations.js
@@ -0,0 +1,56 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_BASE_CONFIG](state, options) {
+ Object.assign(state, { ...options });
+ },
+ [types.SET_TABINDEX](state, tabIndex) {
+ state.tabIndex = tabIndex;
+ },
+ [types.FETCH_COMMITS](state) {
+ state.isLoadingCommits = true;
+ state.commitsLoadingError = false;
+ },
+ [types.SET_COMMITS](state, commits) {
+ state.commits = commits;
+ state.isLoadingCommits = false;
+ state.commitsLoadingError = false;
+ },
+ [types.SET_COMMITS_SILENT](state, commits) {
+ state.commits = commits;
+ },
+ [types.FETCH_COMMITS_ERROR](state) {
+ state.commitsLoadingError = true;
+ state.isLoadingCommits = false;
+ },
+ [types.FETCH_CONTEXT_COMMITS](state) {
+ state.isLoadingContextCommits = true;
+ state.contextCommitsLoadingError = false;
+ },
+ [types.SET_CONTEXT_COMMITS](state, contextCommits) {
+ state.contextCommits = contextCommits;
+ state.isLoadingContextCommits = false;
+ state.contextCommitsLoadingError = false;
+ },
+ [types.FETCH_CONTEXT_COMMITS_ERROR](state) {
+ state.contextCommitsLoadingError = true;
+ state.isLoadingContextCommits = false;
+ },
+ [types.SET_SELECTED_COMMITS](state, commits) {
+ state.selectedCommits = commits;
+ },
+ [types.SET_SEARCH_TEXT](state, searchText) {
+ state.searchText = searchText;
+ },
+ [types.SET_TO_REMOVE_COMMITS](state, commits) {
+ state.toRemoveCommits = commits;
+ },
+ [types.RESET_MODAL_STATE](state) {
+ state.tabIndex = 0;
+ state.commits = [];
+ state.contextCommits = [];
+ state.selectedCommits = [];
+ state.toRemoveCommits = [];
+ state.searchText = '';
+ },
+};
diff --git a/app/assets/javascripts/add_context_commits_modal/store/state.js b/app/assets/javascripts/add_context_commits_modal/store/state.js
new file mode 100644
index 00000000000..37239adccbb
--- /dev/null
+++ b/app/assets/javascripts/add_context_commits_modal/store/state.js
@@ -0,0 +1,13 @@
+export default () => ({
+ contextCommitsPath: '',
+ tabIndex: 0,
+ isLoadingCommits: false,
+ commits: [],
+ commitsLoadingError: false,
+ selectedCommits: [],
+ isLoadingContextCommits: false,
+ contextCommits: [],
+ contextCommitsLoadingError: false,
+ searchText: '',
+ toRemoveCommits: [],
+});
diff --git a/app/assets/javascripts/add_context_commits_modal/utils.js b/app/assets/javascripts/add_context_commits_modal/utils.js
new file mode 100644
index 00000000000..3495ee17cd3
--- /dev/null
+++ b/app/assets/javascripts/add_context_commits_modal/utils.js
@@ -0,0 +1,32 @@
+export const findCommitIndex = (commits, commitShortId) => {
+ return commits.findIndex(commit => commit.short_id === commitShortId);
+};
+
+export const setCommitStatus = (commits, commitIndex, selected) => {
+ const tempCommits = [...commits];
+ tempCommits[commitIndex] = {
+ ...tempCommits[commitIndex],
+ isSelected: selected,
+ };
+ return tempCommits;
+};
+
+export const removeIfReadyToBeRemoved = (toRemoveCommits, commitShortId) => {
+ const tempToRemoveCommits = [...toRemoveCommits];
+ const isPresentInToRemove = tempToRemoveCommits.indexOf(commitShortId);
+ if (isPresentInToRemove !== -1) {
+ tempToRemoveCommits.splice(isPresentInToRemove, 1);
+ }
+
+ return tempToRemoveCommits;
+};
+
+export const removeIfPresent = (selectedCommits, commitShortId) => {
+ const tempSelectedCommits = [...selectedCommits];
+ const selectedCommitsIndex = findCommitIndex(tempSelectedCommits, commitShortId);
+ if (selectedCommitsIndex !== -1) {
+ tempSelectedCommits.splice(selectedCommitsIndex, 1);
+ }
+
+ return tempSelectedCommits;
+};
diff --git a/app/assets/javascripts/admin/statistics_panel/store/actions.js b/app/assets/javascripts/admin/statistics_panel/store/actions.js
index 537025f524c..dd04e492388 100644
--- a/app/assets/javascripts/admin/statistics_panel/store/actions.js
+++ b/app/assets/javascripts/admin/statistics_panel/store/actions.js
@@ -1,6 +1,6 @@
import Api from '~/api';
import { s__ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
@@ -23,6 +23,3 @@ export const receiveStatisticsError = ({ commit }, error) => {
commit(types.RECEIVE_STATISTICS_ERROR, error);
createFlash(s__('AdminDashboard|Error loading the statistics. Please try again'));
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/admin/statistics_panel/store/getters.js b/app/assets/javascripts/admin/statistics_panel/store/getters.js
index 24437bc76bf..2aa34b8f38e 100644
--- a/app/assets/javascripts/admin/statistics_panel/store/getters.js
+++ b/app/assets/javascripts/admin/statistics_panel/store/getters.js
@@ -3,6 +3,7 @@
* and returns an array of the following form:
* [{ key: "forks", label: "Forks", value: 50 }]
*/
+// eslint-disable-next-line import/prefer-default-export
export const getStatistics = state => labels =>
Object.keys(labels).map(key => {
const result = {
@@ -12,6 +13,3 @@ export const getStatistics = state => labels =>
};
return result;
});
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index 0731349630c..5d260fcc200 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -35,13 +35,24 @@ export default {
errorMsg: s__(
'AlertManagement|There was an error displaying the alert. Please refresh the page to try again.',
),
- fullAlertDetailsTitle: s__('AlertManagement|Alert details'),
- overviewTitle: s__('AlertManagement|Overview'),
- metricsTitle: s__('AlertManagement|Metrics'),
reportedAt: s__('AlertManagement|Reported %{when}'),
reportedAtWithTool: s__('AlertManagement|Reported %{when} by %{tool}'),
},
severityLabels: ALERTS_SEVERITY_LABELS,
+ tabsConfig: [
+ {
+ id: 'overview',
+ title: s__('AlertManagement|Overview'),
+ },
+ {
+ id: 'fullDetails',
+ title: s__('AlertManagement|Alert details'),
+ },
+ {
+ id: 'metrics',
+ title: s__('AlertManagement|Metrics'),
+ },
+ ],
components: {
GlBadge,
GlAlert,
@@ -102,8 +113,8 @@ export default {
errored: false,
sidebarStatus: false,
isErrorDismissed: false,
- createIssueError: '',
- issueCreationInProgress: false,
+ createIncidentError: '',
+ incidentCreationInProgress: false,
sidebarErrorMessage: '',
};
},
@@ -119,6 +130,18 @@ export default {
showErrorMsg() {
return this.errored && !this.isErrorDismissed;
},
+ activeTab() {
+ return this.$route.params.tabId || this.$options.tabsConfig[0].id;
+ },
+ currentTabIndex: {
+ get() {
+ return this.$options.tabsConfig.findIndex(tab => tab.id === this.activeTab);
+ },
+ set(tabIdx) {
+ const tabId = this.$options.tabsConfig[tabIdx].id;
+ this.$router.replace({ name: 'tab', params: { tabId } });
+ },
+ },
},
mounted() {
this.trackPageViews();
@@ -149,8 +172,8 @@ export default {
this.errored = true;
this.sidebarErrorMessage = errorMessage;
},
- createIssue() {
- this.issueCreationInProgress = true;
+ createIncident() {
+ this.incidentCreationInProgress = true;
this.$apollo
.mutate({
@@ -162,18 +185,18 @@ export default {
})
.then(({ data: { createAlertIssue: { errors, issue } } }) => {
if (errors?.length) {
- [this.createIssueError] = errors;
- this.issueCreationInProgress = false;
+ [this.createIncidentError] = errors;
+ this.incidentCreationInProgress = false;
} else if (issue) {
- visitUrl(this.issuePath(issue.iid));
+ visitUrl(this.incidentPath(issue.iid));
}
})
.catch(error => {
- this.createIssueError = error;
- this.issueCreationInProgress = false;
+ this.createIncidentError = error;
+ this.incidentCreationInProgress = false;
});
},
- issuePath(issueId) {
+ incidentPath(issueId) {
return joinPaths(this.projectIssuesPath, issueId);
},
trackPageViews() {
@@ -190,12 +213,12 @@ export default {
<p v-html="sidebarErrorMessage || $options.i18n.errorMsg"></p>
</gl-alert>
<gl-alert
- v-if="createIssueError"
+ v-if="createIncidentError"
variant="danger"
- data-testid="issueCreationError"
- @dismiss="createIssueError = null"
+ data-testid="incidentCreationError"
+ @dismiss="createIncidentError = null"
>
- {{ createIssueError }}
+ {{ createIncidentError }}
</gl-alert>
<div v-if="loading"><gl-loading-icon size="lg" class="gl-mt-5" /></div>
<div
@@ -204,19 +227,12 @@ export default {
:class="{ 'pr-sm-8': sidebarStatus }"
>
<div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid flex-column flex-sm-row"
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-flex-direction-column gl-sm-flex-direction-row"
>
- <div
- data-testid="alert-header"
- class="gl-display-flex gl-align-items-center gl-justify-content-center"
- >
- <div
- class="gl-display-inline-flex gl-align-items-center gl-justify-content-space-between"
- >
- <gl-badge class="gl-mr-3">
- <strong>{{ s__('AlertManagement|Alert') }}</strong>
- </gl-badge>
- </div>
+ <div data-testid="alert-header">
+ <gl-badge class="gl-mr-3">
+ <strong>{{ s__('AlertManagement|Alert') }}</strong>
+ </gl-badge>
<span>
<gl-sprintf :message="reportedAtMessage">
<template #when>
@@ -228,24 +244,24 @@ export default {
</div>
<gl-button
v-if="alert.issueIid"
- class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-issue-button"
- data-testid="viewIssueBtn"
- :href="issuePath(alert.issueIid)"
+ class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-incident-button"
+ data-testid="viewIncidentBtn"
+ :href="incidentPath(alert.issueIid)"
category="primary"
variant="success"
>
- {{ s__('AlertManagement|View issue') }}
+ {{ s__('AlertManagement|View incident') }}
</gl-button>
<gl-button
v-else
- class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-issue-button"
- data-testid="createIssueBtn"
- :loading="issueCreationInProgress"
+ class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-incident-button"
+ data-testid="createIncidentBtn"
+ :loading="incidentCreationInProgress"
category="primary"
variant="success"
- @click="createIssue()"
+ @click="createIncident()"
>
- {{ s__('AlertManagement|Create issue') }}
+ {{ s__('AlertManagement|Create incident') }}
</gl-button>
<gl-button
:aria-label="__('Toggle sidebar')"
@@ -264,8 +280,8 @@ export default {
>
<h2 data-testid="title">{{ alert.title }}</h2>
</div>
- <gl-tabs v-if="alert" data-testid="alertDetailsTabs">
- <gl-tab data-testid="overviewTab" :title="$options.i18n.overviewTitle">
+ <gl-tabs v-if="alert" v-model="currentTabIndex" data-testid="alertDetailsTabs">
+ <gl-tab :data-testid="$options.tabsConfig[0].id" :title="$options.tabsConfig[0].title">
<div v-if="alert.severity" class="gl-mt-3 gl-mb-5 gl-display-flex">
<div class="gl-font-weight-bold gl-w-13 gl-text-right gl-pr-3">
{{ s__('AlertManagement|Severity') }}:
@@ -308,6 +324,12 @@ export default {
</div>
<div class="gl-pl-2" data-testid="service">{{ alert.service }}</div>
</div>
+ <div v-if="alert.runbook" class="gl-my-5 gl-display-flex">
+ <div class="bold gl-w-13 gl-text-right gl-pr-3">
+ {{ s__('AlertManagement|Runbook') }}:
+ </div>
+ <div class="gl-pl-2" data-testid="runbook">{{ alert.runbook }}</div>
+ </div>
<template>
<div v-if="alert.notes.nodes" class="issuable-discussion py-5">
<ul class="notes main-notes-list timeline">
@@ -316,7 +338,7 @@ export default {
</div>
</template>
</gl-tab>
- <gl-tab data-testid="fullDetailsTab" :title="$options.i18n.fullAlertDetailsTitle">
+ <gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
<gl-table
class="alert-management-details-table"
:items="[{ key: 'Value', ...alert }]"
@@ -332,7 +354,7 @@ export default {
</template>
</gl-table>
</gl-tab>
- <gl-tab data-testId="metricsTab" :title="$options.i18n.metricsTitle">
+ <gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
<alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
</gl-tab>
</gl-tabs>
diff --git a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
index 13b6a8e6653..68443166f40 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_empty_state.vue
@@ -1,6 +1,7 @@
<script>
-import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { GlEmptyState, GlButton, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
+import alertsHelpUrlQuery from '../graphql/queries/alert_help_url.query.graphql';
export default {
i18n: {
@@ -25,6 +26,12 @@ export default {
components: {
GlEmptyState,
GlButton,
+ GlLink,
+ },
+ apollo: {
+ alertsHelpUrl: {
+ query: alertsHelpUrlQuery,
+ },
},
props: {
enableAlertManagementPath: {
@@ -50,6 +57,11 @@ export default {
default: '',
},
},
+ data() {
+ return {
+ alertsHelpUrl: '',
+ };
+ },
computed: {
emptyState() {
return {
@@ -71,13 +83,9 @@ export default {
<template #description>
<div class="gl-display-block">
<span>{{ emptyState.info }}</span>
- <a
- v-if="!opsgenieMvcEnabled"
- href="/help/user/project/operations/alert_management.html"
- target="_blank"
- >
+ <gl-link v-if="!opsgenieMvcEnabled" :href="alertsHelpUrl" target="_blank">
{{ $options.i18n.moreInformation }}
- </a>
+ </gl-link>
</div>
<div v-if="alertsCanBeEnabled" class="gl-display-block center gl-pt-4">
<gl-button category="primary" variant="success" :href="emptyState.link">
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 7dd3d7b5dc3..92fd85c6217 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -12,8 +12,8 @@ import {
GlSearchBoxByType,
GlSprintf,
} from '@gitlab/ui';
-import { __, s__ } from '~/locale';
import { debounce, trim } from 'lodash';
+import { __, s__ } from '~/locale';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -60,15 +60,15 @@ export default {
{
key: 'severity',
label: s__('AlertManagement|Severity'),
- tdClass: `${tdClass} rounded-top text-capitalize`,
thClass: `${thClass} gl-w-eighth`,
+ tdClass: `${tdClass} rounded-top text-capitalize sortable-cell`,
sortable: true,
},
{
key: 'startedAt',
label: s__('AlertManagement|Start time'),
thClass: `${thClass} js-started-at w-15p`,
- tdClass,
+ tdClass: `${tdClass} sortable-cell`,
sortable: true,
},
{
@@ -81,7 +81,7 @@ export default {
key: 'eventCount',
label: s__('AlertManagement|Events'),
thClass: `${thClass} text-right gl-w-12`,
- tdClass: `${tdClass} text-md-right`,
+ tdClass: `${tdClass} text-md-right sortable-cell`,
sortable: true,
},
{
@@ -89,7 +89,6 @@ export default {
label: s__('AlertManagement|Issue'),
thClass: 'gl-w-12 gl-pointer-events-none',
tdClass,
- sortable: false,
},
{
key: 'assignees',
@@ -99,9 +98,9 @@ export default {
},
{
key: 'status',
- thClass: `${thClass} w-15p`,
label: s__('AlertManagement|Status'),
- tdClass: `${tdClass} rounded-bottom`,
+ thClass: `${thClass} w-15p`,
+ tdClass: `${tdClass} rounded-bottom sortable-cell`,
sortable: true,
},
],
@@ -169,7 +168,7 @@ export default {
};
},
error() {
- this.errored = true;
+ this.hasError = true;
},
},
alertsCount: {
@@ -188,10 +187,9 @@ export default {
data() {
return {
searchTerm: '',
- errored: false,
+ hasError: false,
errorMessage: '',
isAlertDismissed: false,
- isErrorAlertDismissed: false,
sort: 'STARTED_AT_DESC',
statusFilter: [],
filteredByStatus: '',
@@ -204,16 +202,13 @@ export default {
computed: {
showNoAlertsMsg() {
return (
- !this.errored &&
+ !this.hasError &&
!this.loading &&
this.alertsCount?.all === 0 &&
!this.searchTerm &&
!this.isAlertDismissed
);
},
- showErrorMsg() {
- return this.errored && !this.isErrorAlertDismissed;
- },
loading() {
return this.$apollo.queries.alerts.loading;
},
@@ -307,11 +302,11 @@ export default {
};
},
handleAlertError(errorMessage) {
- this.errored = true;
+ this.hasError = true;
this.errorMessage = errorMessage;
},
dismissError() {
- this.isErrorAlertDismissed = true;
+ this.hasError = false;
this.errorMessage = '';
},
},
@@ -319,7 +314,7 @@ export default {
</script>
<template>
<div>
- <div class="alert-management-list">
+ <div class="incident-management-list">
<gl-alert v-if="showNoAlertsMsg" @dismiss="isAlertDismissed = true">
<gl-sprintf :message="$options.i18n.noAlertsMsg">
<template #link="{ content }">
@@ -333,16 +328,14 @@ export default {
</template>
</gl-sprintf>
</gl-alert>
- <gl-alert
- v-if="showErrorMsg"
- variant="danger"
- data-testid="alert-error"
- @dismiss="dismissError"
- >
+ <gl-alert v-if="hasError" variant="danger" data-testid="alert-error" @dismiss="dismissError">
<p v-html="errorMessage || $options.i18n.errorMsg"></p>
</gl-alert>
- <gl-tabs content-class="gl-p-0" @input="filterAlertsByStatus">
+ <gl-tabs
+ content-class="gl-p-0 gl-border-b-solid gl-border-b-1 gl-border-gray-100"
+ @input="filterAlertsByStatus"
+ >
<gl-tab v-for="tab in $options.statusTabs" :key="tab.status">
<template slot="title">
<span>{{ tab.title }}</span>
diff --git a/app/assets/javascripts/alert_management/components/alert_status.vue b/app/assets/javascripts/alert_management/components/alert_status.vue
index 9b726fe2944..8531ca1374e 100644
--- a/app/assets/javascripts/alert_management/components/alert_status.vue
+++ b/app/assets/javascripts/alert_management/components/alert_status.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import { trackAlertStatusUpdateOptions } from '../constants';
@@ -18,8 +18,8 @@ export default {
RESOLVED: s__('AlertManagement|Resolved'),
},
components: {
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
GlButton,
},
props: {
@@ -91,7 +91,7 @@ export default {
<template>
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
- <gl-dropdown
+ <gl-deprecated-dropdown
ref="dropdown"
right
:text="$options.statuses[alert.status]"
@@ -112,7 +112,7 @@ export default {
/>
</div>
<div class="dropdown-content dropdown-body">
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="(label, field) in $options.statuses"
:key="field"
data-testid="statusDropdownItem"
@@ -122,8 +122,8 @@ export default {
@click="updateAlertStatus(label)"
>
{{ label }}
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
</div>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue
index df07038151e..0a1478ef5fe 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignee.vue
@@ -1,9 +1,9 @@
<script>
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdownItem } from '@gitlab/ui';
export default {
components: {
- GlDropdownItem,
+ GlDeprecatedDropdownItem,
},
props: {
user: {
@@ -24,7 +24,7 @@ export default {
</script>
<template>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
:key="user.username"
data-testid="assigneeDropdownItem"
class="assignee-dropdown-item gl-vertical-align-middle"
@@ -47,5 +47,5 @@ export default {
</strong>
<span class="dropdown-menu-user-username"> {{ user.username }}</span>
</span>
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
</template>
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
index cb32a5ffd4f..4af5c83b30c 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue
@@ -1,20 +1,20 @@
<script>
import {
GlIcon,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownHeader,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownDivider,
+ GlDeprecatedDropdownHeader,
+ GlDeprecatedDropdownItem,
GlLoadingIcon,
GlTooltip,
GlButton,
GlSprintf,
} from '@gitlab/ui';
+import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { s__, __ } from '~/locale';
import alertSetAssignees from '../../graphql/mutations/alert_set_assignees.mutation.graphql';
import SidebarAssignee from './sidebar_assignee.vue';
-import { debounce } from 'lodash';
const DATA_REFETCH_DELAY = 250;
@@ -33,10 +33,10 @@ export default {
},
components: {
GlIcon,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlDropdownHeader,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
+ GlDeprecatedDropdownDivider,
+ GlDeprecatedDropdownHeader,
GlLoadingIcon,
GlTooltip,
GlButton,
@@ -213,7 +213,7 @@ export default {
</p>
<div class="dropdown dropdown-menu-selectable" :class="dropdownClass">
- <gl-dropdown
+ <gl-deprecated-dropdown
ref="dropdown"
:text="assignedUser"
class="w-100"
@@ -243,18 +243,18 @@ export default {
</div>
<div class="dropdown-content dropdown-body">
<template v-if="userListValid">
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
:active="!userName"
active-class="is-active"
@click="updateAlertAssignees('')"
>
{{ __('Unassigned') }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
+ </gl-deprecated-dropdown-item>
+ <gl-deprecated-dropdown-divider />
- <gl-dropdown-header class="mt-0">
+ <gl-deprecated-dropdown-header class="mt-0">
{{ __('Assignee') }}
- </gl-dropdown-header>
+ </gl-deprecated-dropdown-header>
<sidebar-assignee
v-for="user in sortedUsers"
:key="user.username"
@@ -263,17 +263,17 @@ export default {
@update-alert-assignees="updateAlertAssignees"
/>
</template>
- <gl-dropdown-item v-else-if="userListEmpty">
+ <gl-deprecated-dropdown-item v-else-if="userListEmpty">
{{ __('No Matching Results') }}
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
<gl-loading-icon v-else />
</div>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</div>
<gl-loading-icon v-if="isUpdating" :inline="true" />
<p v-else-if="!isDropdownShowing" class="value gl-m-0" :class="{ 'no-value': !userName }">
- <span v-if="userName" class="gl-text-gray-700" data-testid="assigned-users">{{
+ <span v-if="userName" class="gl-text-gray-500" data-testid="assigned-users">{{
assignedUser
}}</span>
<span v-else class="gl-display-flex gl-align-items-center">
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue
index fd40b5d9f65..70902a204f8 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_header.vue
@@ -27,7 +27,7 @@ export default {
<template>
<div class="block gl-display-flex gl-justify-content-space-between">
<span class="issuable-header-text hide-collapsed">
- {{ __('To Do') }}
+ {{ __('To-Do') }}
</span>
<sidebar-todo
v-if="!sidebarCollapsed"
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue
index 44a81aba828..0a2bad5510b 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_status.vue
@@ -107,7 +107,7 @@ export default {
>
<span
v-if="$options.statuses[alert.status]"
- class="gl-text-gray-700"
+ class="gl-text-gray-500"
data-testid="status"
>{{ $options.statuses[alert.status] }}</span
>
diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
index 7d3135ad50d..5bd69a1f0ec 100644
--- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
+++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_todo.vue
@@ -1,13 +1,14 @@
<script>
import { s__ } from '~/locale';
import Todo from '~/sidebar/components/todo_toggle/todo.vue';
-import axios from '~/lib/utils/axios_utils';
-import createAlertTodo from '../../graphql/mutations/alert_todo_create.graphql';
+import createAlertTodo from '../../graphql/mutations/alert_todo_create.mutation.graphql';
+import todoMarkDone from '../../graphql/mutations/alert_todo_mark_done.mutation.graphql';
+import alertQuery from '../../graphql/queries/details.query.graphql';
export default {
i18n: {
UPDATE_ALERT_TODO_ERROR: s__(
- 'AlertManagement|There was an error while updating the To Do of the alert.',
+ 'AlertManagement|There was an error while updating the To-Do of the alert.',
),
},
components: {
@@ -30,14 +31,24 @@ export default {
data() {
return {
isUpdating: false,
- isTodo: false,
- todo: '',
};
},
computed: {
alertID() {
return parseInt(this.alert.iid, 10);
},
+ firstToDoId() {
+ return this.alert?.todos?.nodes[0]?.id;
+ },
+ hasPendingTodos() {
+ return this.alert?.todos?.nodes.length > 0;
+ },
+ getAlertQueryVariables() {
+ return {
+ fullPath: this.projectPath,
+ alertId: this.alert.iid,
+ };
+ },
},
methods: {
updateToDoCount(add) {
@@ -51,11 +62,7 @@ export default {
return document.dispatchEvent(headerTodoEvent);
},
- toggleTodo() {
- if (this.todo) {
- return this.markAsDone();
- }
-
+ addToDo() {
this.isUpdating = true;
return this.$apollo
.mutate({
@@ -65,24 +72,14 @@ export default {
projectPath: this.projectPath,
},
})
- .then(({ data: { alertTodoCreate: { todo = {}, errors = [] } } = {} } = {}) => {
+ .then(({ data: { errors = [] } }) => {
if (errors[0]) {
- return this.$emit(
- 'alert-error',
- `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${errors[0]}.`,
- );
+ return this.throwError(errors[0]);
}
-
- this.todo = todo.id;
return this.updateToDoCount(true);
})
.catch(() => {
- this.$emit(
- 'alert-error',
- `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${s__(
- 'AlertManagement|Please try again.',
- )}`,
- );
+ this.throwError();
})
.finally(() => {
this.isUpdating = false;
@@ -90,20 +87,45 @@ export default {
},
markAsDone() {
this.isUpdating = true;
-
- return axios
- .delete(`/dashboard/todos/${this.todo.split('/').pop()}`)
- .then(() => {
- this.todo = '';
+ return this.$apollo
+ .mutate({
+ mutation: todoMarkDone,
+ variables: {
+ id: this.firstToDoId,
+ },
+ update: this.updateCache,
+ })
+ .then(({ data: { errors = [] } }) => {
+ if (errors[0]) {
+ return this.throwError(errors[0]);
+ }
return this.updateToDoCount(false);
})
.catch(() => {
- this.$emit('alert-error', this.$options.i18n.UPDATE_ALERT_TODO_ERROR);
+ this.throwError();
})
.finally(() => {
this.isUpdating = false;
});
},
+ updateCache(store) {
+ const data = store.readQuery({
+ query: alertQuery,
+ variables: this.getAlertQueryVariables,
+ });
+
+ data.project.alertManagementAlerts.nodes[0].todos.nodes.shift();
+
+ store.writeQuery({
+ query: alertQuery,
+ variables: this.getAlertQueryVariables,
+ data,
+ });
+ },
+ throwError(err = '') {
+ const error = err || s__('AlertManagement|Please try again.');
+ this.$emit('alert-error', `${this.$options.i18n.UPDATE_ALERT_TODO_ERROR} ${error}`);
+ },
},
};
</script>
@@ -114,10 +136,10 @@ export default {
data-testid="alert-todo-button"
:collapsed="sidebarCollapsed"
:issuable-id="alertID"
- :is-todo="todo !== ''"
+ :is-todo="hasPendingTodos"
:is-action-active="isUpdating"
issuable-type="alert"
- @toggleTodo="toggleTodo"
+ @toggleTodo="hasPendingTodos ? markAsDone() : addToDo()"
/>
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/alert_management/details.js
index 2820bcb9665..dccf990f0b4 100644
--- a/app/assets/javascripts/alert_management/details.js
+++ b/app/assets/javascripts/alert_management/details.js
@@ -1,7 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import createDefaultClient from '~/lib/graphql';
+import createRouter from './router';
import AlertDetails from './components/alert_details.vue';
import sidebarStatusQuery from './graphql/queries/sidebar_status.query.graphql';
@@ -10,6 +11,7 @@ Vue.use(VueApollo);
export default selector => {
const domEl = document.querySelector(selector);
const { alertId, projectPath, projectIssuesPath, projectId } = domEl.dataset;
+ const router = createRouter();
const resolvers = {
Mutation: {
@@ -54,6 +56,7 @@ export default selector => {
components: {
AlertDetails,
},
+ router,
render(createElement) {
return createElement('alert-details', {});
},
diff --git a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
index 18fab429164..0712ff12c23 100644
--- a/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
+++ b/app/assets/javascripts/alert_management/graphql/fragments/detail_item.fragment.graphql
@@ -11,6 +11,12 @@ fragment AlertDetailItem on AlertManagementAlert {
updatedAt
endedAt
details
+ runbook
+ todos {
+ nodes {
+ id
+ }
+ }
notes {
nodes {
...AlertNote
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql
deleted file mode 100644
index cdf3d763302..00000000000
--- a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.graphql
+++ /dev/null
@@ -1,11 +0,0 @@
-mutation($projectPath: ID!, $iid: String!) {
- alertTodoCreate(input: { iid: $iid, projectPath: $projectPath }) {
- errors
- alert {
- iid
- }
- todo {
- id
- }
- }
-}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql
new file mode 100644
index 00000000000..ac9858c104f
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_create.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/detail_item.fragment.graphql"
+
+mutation alertTodoCreate($projectPath: ID!, $iid: String!) {
+ alertTodoCreate(input: { iid: $iid, projectPath: $projectPath }) {
+ errors
+ alert {
+ ...AlertDetailItem
+ }
+ }
+}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql
new file mode 100644
index 00000000000..4d59b4d94cd
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/mutations/alert_todo_mark_done.mutation.graphql
@@ -0,0 +1,8 @@
+mutation todoMarkDone($id: ID!) {
+ todoMarkDone(input: { id: $id }) {
+ errors
+ todo {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/alert_management/graphql/queries/alert_help_url.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/alert_help_url.query.graphql
new file mode 100644
index 00000000000..05a8bc7c736
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/queries/alert_help_url.query.graphql
@@ -0,0 +1,3 @@
+query alertsHelpUrl {
+ alertsHelpUrl @client
+}
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
index 3f78ca66a59..e180ab5f7e3 100644
--- a/app/assets/javascripts/alert_management/list.js
+++ b/app/assets/javascripts/alert_management/list.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import AlertManagementList from './components/alert_management_list_wrapper.vue';
@@ -16,6 +16,7 @@ export default () => {
enableAlertManagementPath,
emptyAlertSvgPath,
populatingAlertsHelpUrl,
+ alertsHelpUrl,
opsgenieMvcTargetUrl,
} = domEl.dataset;
let { alertManagementEnabled, userCanEnableAlertManagement, opsgenieMvcEnabled } = domEl.dataset;
@@ -41,6 +42,12 @@ export default () => {
),
});
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ alertsHelpUrl,
+ },
+ });
+
return new Vue({
el: selector,
apolloProvider,
diff --git a/app/assets/javascripts/alert_management/router.js b/app/assets/javascripts/alert_management/router.js
new file mode 100644
index 00000000000..5687fe4e0f5
--- /dev/null
+++ b/app/assets/javascripts/alert_management/router.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { joinPaths } from '~/lib/utils/url_utility';
+
+Vue.use(VueRouter);
+
+export default function createRouter(base) {
+ return new VueRouter({
+ mode: 'hash',
+ base: joinPaths(gon.relative_url_root || '', base),
+ routes: [{ path: '/:tabId', name: 'tab' }],
+ });
+}
diff --git a/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue b/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue
index a2d94fb8083..c5e213d7dc9 100644
--- a/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue
+++ b/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue
@@ -12,7 +12,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import axios from '~/lib/utils/axios_utils';
import { s__, __ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
export default {
i18n: {
@@ -180,9 +180,11 @@ export default {
/>
</span>
</div>
- <gl-button v-gl-modal.authKeyModal class="mt-2" :disabled="isDisabled">{{
- $options.RESET_KEY
- }}</gl-button>
+ <span class="gl-display-flex gl-justify-content-end">
+ <gl-button v-gl-modal.authKeyModal class="gl-mt-2" :disabled="isDisabled">{{
+ $options.RESET_KEY
+ }}</gl-button>
+ </span>
<gl-modal
modal-id="authKeyModal"
:title="$options.RESET_KEY"
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index 18c9f82f052..f0bb8b0a90f 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -51,52 +51,26 @@ export default {
'gl-modal': GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
- props: {
- prometheus: {
- type: Object,
- required: true,
- validator: ({ activated }) => {
- return activated !== undefined;
- },
- },
- generic: {
- type: Object,
- required: true,
- validator: ({ formPath }) => {
- return formPath !== undefined;
- },
- },
- opsgenie: {
- type: Object,
- required: true,
- },
- },
+ inject: ['prometheus', 'generic', 'opsgenie'],
data() {
return {
- activated: {
- generic: this.generic.activated,
- prometheus: this.prometheus.activated,
- opsgenie: this.opsgenie?.activated,
- },
loading: false,
- authorizationKey: {
- generic: this.generic.initialAuthorizationKey,
- prometheus: this.prometheus.prometheusAuthorizationKey,
- },
selectedEndpoint: serviceOptions[0].value,
options: serviceOptions,
- targetUrl: null,
+ active: false,
+ authKey: '',
+ targetUrl: '',
feedback: {
variant: 'danger',
- feedbackMessage: null,
+ feedbackMessage: '',
isFeedbackDismissed: false,
},
- serverError: null,
testAlert: {
json: null,
error: null,
},
canSaveForm: false,
+ serverError: null,
};
},
computed: {
@@ -123,24 +97,24 @@ export default {
case 'generic': {
return {
url: this.generic.url,
- authKey: this.authorizationKey.generic,
- active: this.activated.generic,
- resetKey: this.resetGenericKey.bind(this),
+ authKey: this.generic.authorizationKey,
+ activated: this.generic.activated,
+ resetKey: this.resetKey.bind(this),
};
}
case 'prometheus': {
return {
url: this.prometheus.prometheusUrl,
- authKey: this.authorizationKey.prometheus,
- active: this.activated.prometheus,
- resetKey: this.resetPrometheusKey.bind(this),
+ authKey: this.prometheus.authorizationKey,
+ activated: this.prometheus.activated,
+ resetKey: this.resetKey.bind(this, 'prometheus'),
targetUrl: this.prometheus.prometheusApiUrl,
};
}
case 'opsgenie': {
return {
targetUrl: this.opsgenie.opsgenieMvcTargetUrl,
- active: this.activated.opsgenie,
+ activated: this.opsgenie.activated,
};
}
default: {
@@ -164,7 +138,7 @@ export default {
return this.testAlert.error === null;
},
canTestAlert() {
- return this.selectedService.active && this.testAlert.json !== null;
+ return this.active && this.testAlert.json !== null;
},
canSaveConfig() {
return !this.loading && this.canSaveForm;
@@ -187,19 +161,21 @@ export default {
},
mounted() {
if (
- this.activated.prometheus ||
- this.activated.generic ||
+ this.prometheus.activated ||
+ this.generic.activated ||
!this.opsgenie.opsgenieMvcIsAvailable
) {
this.removeOpsGenieOption();
- } else if (this.activated.opsgenie) {
+ } else if (this.opsgenie.activated) {
this.setOpsgenieAsDefault();
}
+ this.active = this.selectedService.activated;
+ this.authKey = this.selectedService.authKey ?? '';
},
methods: {
- createUserErrorMessage(errors) {
+ createUserErrorMessage(errors = { error: [''] }) {
// eslint-disable-next-line prefer-destructuring
- this.serverError = Object.values(errors)[0][0];
+ this.serverError = errors.error[0];
},
setOpsgenieAsDefault() {
this.options = this.options.map(el => {
@@ -224,41 +200,38 @@ export default {
resetFormValues() {
this.testAlert.json = null;
this.targetUrl = this.selectedService.targetUrl;
+ this.active = this.selectedService.activated;
},
dismissFeedback() {
this.serverError = null;
this.feedback = { ...this.feedback, feedbackMessage: null };
this.isFeedbackDismissed = false;
},
- resetGenericKey() {
- return service
- .updateGenericKey({ endpoint: this.generic.formPath, params: { service: { token: '' } } })
+ resetKey(key) {
+ const fn = key === 'prometheus' ? this.resetPrometheusKey() : this.resetGenericKey();
+
+ return fn
.then(({ data: { token } }) => {
- this.authorizationKey.generic = token;
+ this.authKey = token;
this.setFeedback({ feedbackMessage: this.$options.i18n.authKeyRest, variant: 'success' });
})
.catch(() => {
this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
});
},
+ resetGenericKey() {
+ this.dismissFeedback();
+ return service.updateGenericKey({
+ endpoint: this.generic.formPath,
+ params: { service: { token: '' } },
+ });
+ },
resetPrometheusKey() {
- return service
- .updatePrometheusKey({ endpoint: this.prometheus.prometheusResetKeyPath })
- .then(({ data: { token } }) => {
- this.authorizationKey.prometheus = token;
- this.setFeedback({ feedbackMessage: this.$options.i18n.authKeyRest, variant: 'success' });
- })
- .catch(() => {
- this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
- });
+ return service.updatePrometheusKey({ endpoint: this.prometheus.prometheusResetKeyPath });
},
toggleService(value) {
this.canSaveForm = true;
- if (this.isPrometheus) {
- this.activated.prometheus = value;
- } else {
- this.activated[this.selectedEndpoint] = value;
- }
+ this.active = value;
},
toggle(value) {
return this.isPrometheus ? this.togglePrometheusActive(value) : this.toggleActivated(value);
@@ -273,7 +246,7 @@ export default {
: { service: { active: value } },
})
.then(() => {
- this.activated[this.selectedEndpoint] = value;
+ this.active = value;
this.toggleSuccess(value);
if (!this.isOpsgenie && value) {
@@ -316,7 +289,7 @@ export default {
},
})
.then(() => {
- this.activated.prometheus = value;
+ this.active = value;
this.toggleSuccess(value);
this.removeOpsGenieOption();
})
@@ -358,6 +331,7 @@ export default {
},
validateTestAlert() {
this.loading = true;
+ this.dismissFeedback();
this.validateJson();
return service
.updateTestAlert({
@@ -382,7 +356,8 @@ export default {
});
},
onSubmit() {
- this.toggle(this.selectedService.active);
+ this.dismissFeedback();
+ this.toggle(this.active);
},
onReset() {
this.testAlert.json = null;
@@ -391,7 +366,7 @@ export default {
if (this.canSaveForm) {
this.canSaveForm = false;
- this.activated[this.selectedEndpoint] = this[this.selectedEndpoint].activated;
+ this.active = this.selectedService.activated;
}
},
},
@@ -409,7 +384,7 @@ export default {
variant="danger"
category="primary"
class="gl-display-block gl-mt-3"
- @click="toggle(selectedService.active)"
+ @click="toggle(active)"
>
{{ __('Save anyway') }}
</gl-button>
@@ -435,7 +410,7 @@ export default {
data-testid="alert-settings-select"
@change="resetFormValues"
/>
- <span class="gl-text-gray-400">
+ <span class="gl-text-gray-200">
<gl-sprintf :message="$options.i18n.integrationsInfo">
<template #link="{ content }">
<gl-link
@@ -457,7 +432,7 @@ export default {
id="activated"
:disabled-input="loading"
:is-loading="loading"
- :value="selectedService.active"
+ :value="active"
@change="toggleService"
/>
</gl-form-group>
@@ -472,9 +447,9 @@ export default {
v-model="targetUrl"
type="url"
:placeholder="baseUrlPlaceholder"
- :disabled="!selectedService.active"
+ :disabled="!active"
/>
- <span class="gl-text-gray-400">
+ <span class="gl-text-gray-200">
{{ $options.i18n.apiBaseUrlHelpText }}
</span>
</gl-form-group>
@@ -489,7 +464,7 @@ export default {
/>
</template>
</gl-form-input-group>
- <span class="gl-text-gray-400">
+ <span class="gl-text-gray-200">
{{ prometheusInfo }}
</span>
</gl-form-group>
@@ -498,21 +473,16 @@ export default {
label-for="authorization-key"
label-class="label-bold"
>
- <gl-form-input-group
- id="authorization-key"
- class="gl-mb-2"
- readonly
- :value="selectedService.authKey"
- >
+ <gl-form-input-group id="authorization-key" class="gl-mb-2" readonly :value="authKey">
<template #append>
<clipboard-button
- :text="selectedService.authKey || ''"
+ :text="authKey"
:title="$options.i18n.copyToClipboard"
class="gl-m-0!"
/>
</template>
</gl-form-input-group>
- <gl-button v-gl-modal.authKeyModal :disabled="!selectedService.active" class="gl-mt-3">{{
+ <gl-button v-gl-modal.authKeyModal :disabled="!active" class="gl-mt-3">{{
$options.i18n.resetKey
}}</gl-button>
<gl-modal
@@ -534,18 +504,23 @@ export default {
<gl-form-textarea
id="alert-json"
v-model.trim="testAlert.json"
- :disabled="!selectedService.active"
+ :disabled="!active"
:state="jsonIsValid"
:placeholder="$options.i18n.alertJsonPlaceholder"
rows="6"
max-rows="10"
/>
</gl-form-group>
- <gl-button :disabled="!canTestAlert" @click="validateTestAlert">{{
- $options.i18n.testAlertInfo
- }}</gl-button>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button :disabled="!canTestAlert" @click="validateTestAlert">{{
+ $options.i18n.testAlertInfo
+ }}</gl-button>
+ </div>
</template>
<div class="footer-block row-content-block gl-display-flex gl-justify-content-space-between">
+ <gl-button category="primary" :disabled="!canSaveConfig" @click="onReset">
+ {{ __('Cancel') }}
+ </gl-button>
<gl-button
variant="success"
category="primary"
@@ -554,9 +529,6 @@ export default {
>
{{ __('Save changes') }}
</gl-button>
- <gl-button variant="default" category="primary" :disabled="!canSaveConfig" @click="onReset">
- {{ __('Cancel') }}
- </gl-button>
</div>
</gl-form>
</div>
diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js
index d15e8619df4..fc669995875 100644
--- a/app/assets/javascripts/alerts_settings/constants.js
+++ b/app/assets/javascripts/alerts_settings/constants.js
@@ -35,7 +35,9 @@ export const i18n = {
testAlertSuccess: s__(
'AlertSettings|Test alert sent successfully. If you have made other changes, please save them now.',
),
- authKeyRest: s__('AlertSettings|Authorization key has been successfully reset'),
+ authKeyRest: s__(
+ 'AlertSettings|Authorization key has been successfully reset. Please save your changes now.',
+ ),
};
export const serviceOptions = [
diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js
index a4c2bf6b18e..8d1d342d229 100644
--- a/app/assets/javascripts/alerts_settings/index.js
+++ b/app/assets/javascripts/alerts_settings/index.js
@@ -31,37 +31,37 @@ export default el => {
const opsgenieMvcActivated = parseBoolean(opsgenieMvcEnabled);
const opsgenieMvcIsAvailable = parseBoolean(opsgenieMvcAvailable);
- const props = {
- prometheus: {
- activated: prometheusIsActivated,
- prometheusUrl,
- prometheusAuthorizationKey,
- prometheusFormPath,
- prometheusResetKeyPath,
- prometheusApiUrl,
- },
- generic: {
- alertsSetupUrl,
- alertsUsageUrl,
- activated: genericActivated,
- formPath,
- initialAuthorizationKey: authorizationKey,
- url,
- },
- opsgenie: {
- formPath: opsgenieMvcFormPath,
- activated: opsgenieMvcActivated,
- opsgenieMvcTargetUrl,
- opsgenieMvcIsAvailable,
- },
- };
-
return new Vue({
el,
+ provide: {
+ prometheus: {
+ activated: prometheusIsActivated,
+ prometheusUrl,
+ authorizationKey: prometheusAuthorizationKey,
+ prometheusFormPath,
+ prometheusResetKeyPath,
+ prometheusApiUrl,
+ },
+ generic: {
+ alertsSetupUrl,
+ alertsUsageUrl,
+ activated: genericActivated,
+ formPath,
+ authorizationKey,
+ url,
+ },
+ opsgenie: {
+ formPath: opsgenieMvcFormPath,
+ activated: opsgenieMvcActivated,
+ opsgenieMvcTargetUrl,
+ opsgenieMvcIsAvailable,
+ },
+ },
+ components: {
+ AlertSettingsForm,
+ },
render(createElement) {
- return createElement(AlertSettingsForm, {
- props,
- });
+ return createElement('alert-settings-form');
},
});
};
diff --git a/app/assets/javascripts/analytics/cycle_analytics/mixins/filter_mixins.js b/app/assets/javascripts/analytics/cycle_analytics/mixins/filter_mixins.js
deleted file mode 100644
index ff8b4c56321..00000000000
--- a/app/assets/javascripts/analytics/cycle_analytics/mixins/filter_mixins.js
+++ /dev/null
@@ -1 +0,0 @@
-export default {};
diff --git a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js b/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js
new file mode 100644
index 00000000000..d1f4b537b11
--- /dev/null
+++ b/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import ActivityChart from './components/activity_chart.vue';
+
+export default () => {
+ const containers = document.querySelectorAll('.js-project-analytics-chart');
+
+ if (!containers) {
+ return false;
+ }
+
+ return containers.forEach(container => {
+ const { chartData } = container.dataset;
+ const formattedData = JSON.parse(chartData);
+
+ return new Vue({
+ el: container,
+ provide: {
+ formattedData,
+ },
+ components: {
+ ActivityChart,
+ },
+ render(createElement) {
+ return createElement('activity-chart');
+ },
+ });
+ });
+};
diff --git a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue b/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
new file mode 100644
index 00000000000..a475ff8fd25
--- /dev/null
+++ b/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ noDataMsg: s__(
+ 'ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already.',
+ ),
+ },
+ components: {
+ GlColumnChart,
+ },
+ inject: {
+ formattedData: {
+ default: {},
+ },
+ },
+ computed: {
+ seriesData() {
+ return {
+ full: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-xs-w-full">
+ <gl-column-chart
+ v-if="formattedData.keys"
+ :data="seriesData"
+ :x-axis-title="__('Value')"
+ :y-axis-title="__('Number of events')"
+ :x-axis-type="'category'"
+ />
+ <p v-else data-testid="noActivityChartData">
+ {{ $options.i18n.noDataMsg }}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index c84e73ccdb4..1d8fb1fc5a6 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,6 +1,6 @@
import axios from './lib/utils/axios_utils';
import { joinPaths } from './lib/utils/url_utility';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
const DEFAULT_PER_PAGE = 20;
@@ -9,6 +9,7 @@ const Api = {
groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id',
groupMembersPath: '/api/:version/groups/:id/members',
+ groupMilestonesPath: '/api/:version/groups/:id/milestones',
subgroupsPath: '/api/:version/groups/:id/subgroups',
namespacesPath: '/api/:version/namespaces.json',
groupPackagesPath: '/api/:version/groups/:id/packages',
@@ -55,10 +56,14 @@ const Api = {
adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
pipelinesPath: '/api/:version/projects/:id/pipelines/',
+ createPipelinePath: '/api/:version/projects/:id/pipeline',
environmentsPath: '/api/:version/projects/:id/environments',
+ contextCommitsPath:
+ '/api/:version/projects/:id/merge_requests/:merge_request_iid/context_commits',
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
tagsPath: '/api/:version/projects/:id/repository/tags',
+ freezePeriodsPath: '/api/:version/projects/:id/freeze_periods',
group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@@ -106,6 +111,17 @@ const Api = {
});
},
+ groupMilestones(id, options) {
+ const url = Api.buildUrl(this.groupMilestonesPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url, {
+ params: {
+ per_page: DEFAULT_PER_PAGE,
+ ...options,
+ },
+ });
+ },
+
// Return groups list. Filtered by query
groups(query, options, callback = () => {}) {
const url = Api.buildUrl(Api.groupsPath);
@@ -528,6 +544,12 @@ const Api = {
return axios.get(url);
},
+ createRelease(projectPath, release) {
+ const url = Api.buildUrl(this.releasesPath).replace(':id', encodeURIComponent(projectPath));
+
+ return axios.post(url, release);
+ },
+
updateRelease(projectPath, tagName, release) {
const url = Api.buildUrl(this.releasePath)
.replace(':id', encodeURIComponent(projectPath))
@@ -575,11 +597,45 @@ const Api = {
});
},
+ createPipeline(id, data) {
+ const url = Api.buildUrl(this.createPipelinePath).replace(':id', encodeURIComponent(id));
+
+ return axios.post(url, data, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ },
+
environments(id) {
const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id));
return axios.get(url);
},
+ createContextCommits(id, mergeRequestIid, data) {
+ const url = Api.buildUrl(this.contextCommitsPath)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':merge_request_iid', mergeRequestIid);
+
+ return axios.post(url, data);
+ },
+
+ allContextCommits(id, mergeRequestIid) {
+ const url = Api.buildUrl(this.contextCommitsPath)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':merge_request_iid', mergeRequestIid);
+
+ return axios.get(url);
+ },
+
+ removeContextCommits(id, mergeRequestIid, data) {
+ const url = Api.buildUrl(this.contextCommitsPath)
+ .replace(':id', id)
+ .replace(':merge_request_iid', mergeRequestIid);
+
+ return axios.delete(url, { data });
+ },
+
getRawFile(id, path, params = { ref: 'master' }) {
const url = Api.buildUrl(this.rawFilePath)
.replace(':id', encodeURIComponent(id))
@@ -616,6 +672,18 @@ const Api = {
});
},
+ freezePeriods(id) {
+ const url = Api.buildUrl(this.freezePeriodsPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url);
+ },
+
+ createFreezePeriod(id, freezePeriod = {}) {
+ const url = Api.buildUrl(this.freezePeriodsPath).replace(':id', encodeURIComponent(id));
+
+ return axios.post(url, freezePeriod);
+ },
+
buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
},
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 0e83ba3d528..cb71047e00c 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -7,7 +7,7 @@ import Cookies from 'js-cookie';
import { __ } from './locale';
import { updateTooltipTitle } from './lib/utils/common_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import * as Emoji from '~/emoji';
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 4145a4a4145..08834df0a9b 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -2,7 +2,7 @@
import { escape, debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import createEmptyBadge from '../empty_badge';
@@ -184,7 +184,7 @@ export default {
@input="debouncedPreview"
/>
<div class="invalid-feedback">{{ s__('Badges|Please fill in a valid URL') }}</div>
- <span class="form-text text-muted"> {{ badgeLinkUrlExample }} </span>
+ <span class="form-text text-muted">{{ badgeLinkUrlExample }}</span>
</div>
<div class="form-group">
@@ -199,7 +199,7 @@ export default {
@input="debouncedPreview"
/>
<div class="invalid-feedback">{{ s__('Badges|Please fill in a valid URL') }}</div>
- <span class="form-text text-muted"> {{ badgeImageUrlExample }} </span>
+ <span class="form-text text-muted">{{ badgeImageUrlExample }}</span>
</div>
<div class="form-group">
@@ -210,22 +210,26 @@ export default {
:image-url="renderedImageUrl"
:link-url="renderedLinkUrl"
/>
- <p v-show="isRendering"><gl-loading-icon :inline="true" /></p>
+ <p v-show="isRendering">
+ <gl-loading-icon :inline="true" />
+ </p>
<p v-show="!renderedBadge && !isRendering" class="disabled-content">
{{ s__('Badges|No image to preview') }}
</p>
</div>
- <div v-if="isEditing" class="row-content-block">
+ <div v-if="isEditing" class="row-content-block gl-display-flex gl-justify-content-end">
+ <button class="btn btn-cancel gl-mr-4" type="button" @click="onCancel">
+ {{ __('Cancel') }}
+ </button>
<loading-button
:loading="isSaving"
:label="s__('Badges|Save changes')"
type="submit"
container-class="btn btn-success"
/>
- <button class="btn btn-cancel" type="button" @click="onCancel">{{ __('Cancel') }}</button>
</div>
- <div v-else class="form-group">
+ <div v-else class="gl-display-flex gl-justify-content-end form-group">
<loading-button
:loading="isSaving"
:label="s__('Badges|Add badge')"
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
index 531f84ad272..531742e49e3 100644
--- a/app/assets/javascripts/badges/components/badge_settings.vue
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -1,6 +1,7 @@
<script>
import { mapState, mapActions } from 'vuex';
-import createFlash from '~/flash';
+import { GlSprintf } from '@gitlab/ui';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import Badge from './badge.vue';
@@ -14,14 +15,15 @@ export default {
BadgeForm,
BadgeList,
GlModal: DeprecatedModal2,
+ GlSprintf,
+ },
+ i18n: {
+ deleteModalText: s__(
+ 'Badges|You are going to delete this badge. Deleted badges %{strongStart}cannot%{strongEnd} be restored.',
+ ),
},
computed: {
...mapState(['badgeInModal', 'isEditing']),
- deleteModalText() {
- return s__(
- 'Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.',
- );
- },
},
methods: {
...mapActions(['deleteBadge']),
@@ -54,7 +56,13 @@ export default {
:link-url="badgeInModal ? badgeInModal.renderedLinkUrl : ''"
/>
</div>
- <p v-html="deleteModalText"></p>
+ <p>
+ <gl-sprintf :message="$options.i18n.deleteModalText">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
</gl-modal>
<badge-form v-show="isEditing" :is-editing="true" />
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index 4c100ec7335..39c1b8decee 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -1,15 +1,17 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
+import { GlButton } from '@gitlab/ui';
import NoteableNote from '~/notes/components/noteable_note.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import PublishButton from './publish_button.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
NoteableNote,
PublishButton,
- LoadingButton,
+ GlButton,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
draft: {
type: Object,
@@ -64,14 +66,27 @@ export default {
handleNotEditing() {
this.isEditingDraft = false;
},
+ handleMouseEnter(draft) {
+ if (this.glFeatures.multilineComments && draft.position) {
+ this.setSelectedCommentPositionHover(draft.position.line_range);
+ }
+ },
+ handleMouseLeave(draft) {
+ // Even though position isn't used here we still don't want to unecessarily call a mutation
+ // The lack of position tells us that highlighting is irrelevant in this context
+ if (this.glFeatures.multilineComments && draft.position) {
+ this.setSelectedCommentPositionHover();
+ }
+ },
},
};
</script>
<template>
<article
+ role="article"
class="draft-note-component note-wrapper"
- @mouseenter="setSelectedCommentPositionHover(draft.position.line_range)"
- @mouseleave="setSelectedCommentPositionHover()"
+ @mouseenter="handleMouseEnter(draft)"
+ @mouseleave="handleMouseLeave(draft)"
>
<ul class="notes draft-notes">
<noteable-note
@@ -100,18 +115,15 @@ export default {
></div>
<p class="draft-note-actions d-flex">
- <publish-button
- :show-count="true"
- :should-publish="false"
- class="btn btn-success btn-inverted gl-mr-3"
- />
- <loading-button
+ <publish-button :show-count="true" :should-publish="false" category="secondary" />
+ <gl-button
ref="publishNowButton"
:loading="isPublishingDraft(draft.id) || isPublishing"
- :label="__('Add comment now')"
- container-class="btn btn-inverted"
+ class="gl-ml-3"
@click="publishNow"
- />
+ >
+ {{ __('Add comment now') }}
+ </gl-button>
</p>
</template>
</article>
diff --git a/app/assets/javascripts/batch_comments/components/drafts_count.vue b/app/assets/javascripts/batch_comments/components/drafts_count.vue
index f1180760c4d..7a8482ac341 100644
--- a/app/assets/javascripts/batch_comments/components/drafts_count.vue
+++ b/app/assets/javascripts/batch_comments/components/drafts_count.vue
@@ -1,15 +1,19 @@
<script>
import { mapGetters } from 'vuex';
+import { GlBadge } from '@gitlab/ui';
export default {
+ components: {
+ GlBadge,
+ },
computed: {
...mapGetters('batchComments', ['draftsCount']),
},
};
</script>
<template>
- <span class="drafts-count-component">
- <span class="drafts-count-number">{{ draftsCount }}</span>
+ <gl-badge size="sm" variant="success">
+ {{ draftsCount }}
<span class="sr-only"> {{ n__('draft', 'drafts', draftsCount) }} </span>
- </span>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/batch_comments/components/preview_item.vue b/app/assets/javascripts/batch_comments/components/preview_item.vue
index 3162a83f099..982fb01f49a 100644
--- a/app/assets/javascripts/batch_comments/components/preview_item.vue
+++ b/app/assets/javascripts/batch_comments/components/preview_item.vue
@@ -1,10 +1,10 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import { GlSprintf } from '@gitlab/ui';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import resolvedStatusMixin from '../mixins/resolved_status';
-import { GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
getStartLineNumber,
diff --git a/app/assets/javascripts/batch_comments/components/publish_button.vue b/app/assets/javascripts/batch_comments/components/publish_button.vue
index f4dc0f04dc3..0c79e185f06 100644
--- a/app/assets/javascripts/batch_comments/components/publish_button.vue
+++ b/app/assets/javascripts/batch_comments/components/publish_button.vue
@@ -1,12 +1,12 @@
<script>
import { mapActions, mapState } from 'vuex';
+import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import DraftsCount from './drafts_count.vue';
export default {
components: {
- LoadingButton,
+ GlButton,
DraftsCount,
},
props: {
@@ -20,6 +20,16 @@ export default {
required: false,
default: __('Finish review'),
},
+ category: {
+ type: String,
+ required: false,
+ default: 'primary',
+ },
+ variant: {
+ type: String,
+ required: false,
+ default: 'success',
+ },
shouldPublish: {
type: Boolean,
required: true,
@@ -42,14 +52,14 @@ export default {
</script>
<template>
- <loading-button
+ <gl-button
:loading="isPublishing"
- container-class="btn btn-success js-publish-draft-button qa-submit-review"
+ class="js-publish-draft-button qa-submit-review"
+ :category="category"
+ :variant="variant"
@click="onClick"
>
- <span>
- {{ label }}
- <drafts-count v-if="showCount" />
- </span>
- </loading-button>
+ {{ label }}
+ <drafts-count v-if="showCount" />
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue
index b0e8b806701..2d7b86d2431 100644
--- a/app/assets/javascripts/batch_comments/components/review_bar.vue
+++ b/app/assets/javascripts/batch_comments/components/review_bar.vue
@@ -1,13 +1,12 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import PreviewDropdown from './preview_dropdown.vue';
export default {
components: {
- LoadingButton,
+ GlButton,
GlModal,
PreviewDropdown,
},
@@ -48,12 +47,13 @@ export default {
<nav class="review-bar-component">
<div class="review-bar-content qa-review-bar">
<preview-dropdown />
- <loading-button
+ <gl-button
v-gl-modal="$options.modalId"
:loading="isDiscarding"
- :label="__('Discard review')"
class="qa-discard-review float-right"
- />
+ >
+ {{ __('Discard review') }}
+ </gl-button>
</div>
</nav>
<gl-modal
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 1ef012696c5..d9b92113103 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,4 +1,4 @@
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import { scrollToElement } from '~/lib/utils/common_utils';
import service from '../../../services/drafts_service';
@@ -146,6 +146,3 @@ export const expandAllDiscussions = ({ dispatch, state }) =>
export const toggleResolveDiscussion = ({ commit }, draftId) => {
commit(types.TOGGLE_RESOLVE_DISCUSSION, draftId);
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
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 43f43c983aa..22ae6c2e970 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
@@ -82,6 +82,3 @@ export const isPublishingDraft = state => draftId =>
state.currentlyPublishingDrafts.indexOf(draftId) !== -1;
export const sortedDrafts = state => [...state.drafts].sort((a, b) => a.id > b.id);
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index bbcfa50ba35..ce5b63df19c 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -180,6 +180,10 @@ export class CopyAsGFM {
})
.catch(() => {});
}
+
+ static quoted(markdown) {
+ return `> ${markdown.split('\n').join('\n> ')}`;
+ }
}
// Export CopyAsGFM as a global for rspec to access
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index b5dbdbb7e86..03d9955f8fc 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -1,4 +1,4 @@
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { s__, sprintf } from '~/locale';
// Renders math using KaTeX in any element with the
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index 94033e914ef..cb0e6345059 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -1,7 +1,7 @@
-import flash from '~/flash';
import $ from 'jquery';
-import { __, sprintf } from '~/locale';
import { once } from 'lodash';
+import { deprecatedCreateFlash as flash } from '~/flash';
+import { __, sprintf } from '~/locale';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class.
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index ca91400eac7..84bf22586a9 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
// MarkdownPreview
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 530ab0bd4d9..49eab3e4f09 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -72,7 +72,7 @@ $(document).on(
$this.tooltip({
container: 'body',
- html: 'true',
+ html: true,
placement: 'top',
title,
trigger: 'manual',
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index 2e537d8c000..fc86f630c4e 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -1,4 +1,4 @@
-import Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/blob/components/blob_content_error.vue b/app/assets/javascripts/blob/components/blob_content_error.vue
index 44dc4a6c727..7344b9cdff5 100644
--- a/app/assets/javascripts/blob/components/blob_content_error.vue
+++ b/app/assets/javascripts/blob/components/blob_content_error.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { GlSprintf, GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
import { BLOB_RENDER_ERRORS } from './constants';
export default {
diff --git a/app/assets/javascripts/blob/components/blob_edit_content.vue b/app/assets/javascripts/blob/components/blob_edit_content.vue
index 056b4ea4aa8..26ba7b98a39 100644
--- a/app/assets/javascripts/blob/components/blob_edit_content.vue
+++ b/app/assets/javascripts/blob/components/blob_edit_content.vue
@@ -1,6 +1,12 @@
<script>
-import { initEditorLite } from '~/blob/utils';
import { debounce } from 'lodash';
+import { initEditorLite } from '~/blob/utils';
+import {
+ SNIPPET_MARK_BLOBS_CONTENT,
+ SNIPPET_MARK_EDIT_APP_START,
+ SNIPPET_MEASURE_BLOBS_CONTENT,
+ SNIPPET_MEASURE_BLOBS_CONTENT_WITHIN_APP,
+} from '~/performance_constants';
export default {
props: {
@@ -14,6 +20,13 @@ export default {
required: false,
default: '',
},
+ // This is used to help uniquely create a monaco model
+ // even if two blob's share a file path.
+ fileGlobalId: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -30,17 +43,33 @@ export default {
el: this.$refs.editor,
blobPath: this.fileName,
blobContent: this.value,
+ blobGlobalId: this.fileGlobalId,
+ });
+
+ this.editor.onChangeContent(debounce(this.onFileChange.bind(this), 250));
+
+ window.requestAnimationFrame(() => {
+ if (!performance.getEntriesByName(SNIPPET_MARK_BLOBS_CONTENT).length) {
+ performance.mark(SNIPPET_MARK_BLOBS_CONTENT);
+ performance.measure(SNIPPET_MEASURE_BLOBS_CONTENT);
+ performance.measure(SNIPPET_MEASURE_BLOBS_CONTENT_WITHIN_APP, SNIPPET_MARK_EDIT_APP_START);
+ }
});
},
+ beforeDestroy() {
+ this.editor.dispose();
+ },
methods: {
- triggerFileChange: debounce(function debouncedFileChange() {
+ onFileChange() {
this.$emit('input', this.editor.getValue());
- }, 250),
+ },
},
};
</script>
<template>
<div class="file-content code">
- <pre id="editor" ref="editor" data-editor-loading @keyup="triggerFileChange">{{ value }}</pre>
+ <div id="editor" ref="editor" data-editor-loading>
+ <pre class="editor-loading-content">{{ value }}</pre>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/blob/components/blob_edit_header.vue b/app/assets/javascripts/blob/components/blob_edit_header.vue
index e1e1d76f721..2cbbbddceeb 100644
--- a/app/assets/javascripts/blob/components/blob_edit_header.vue
+++ b/app/assets/javascripts/blob/components/blob_edit_header.vue
@@ -1,9 +1,10 @@
<script>
-import { GlFormInput } from '@gitlab/ui';
+import { GlFormInput, GlButton } from '@gitlab/ui';
export default {
components: {
GlFormInput,
+ GlButton,
},
inheritAttrs: false,
props: {
@@ -11,6 +12,16 @@ export default {
type: String,
required: true,
},
+ canDelete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ showDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -21,17 +32,27 @@ export default {
</script>
<template>
<div class="js-file-title file-title-flex-parent">
- <gl-form-input
- id="snippet_file_name"
- v-model="name"
- :placeholder="
- s__('Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby')
- "
- name="snippet_file_name"
- class="form-control js-snippet-file-name"
- type="text"
- v-bind="$attrs"
- @change="$emit('input', name)"
- />
+ <div class="gl-display-flex gl-align-items-center gl-w-full">
+ <gl-form-input
+ v-model="name"
+ :placeholder="
+ s__('Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby')
+ "
+ name="snippet_file_name"
+ class="form-control js-snippet-file-name"
+ type="text"
+ v-bind="$attrs"
+ @change="$emit('input', name)"
+ />
+ <gl-button
+ v-if="showDelete"
+ class="gl-ml-4"
+ variant="danger"
+ category="secondary"
+ :disabled="!canDelete"
+ @click="$emit('delete')"
+ >{{ s__('Snippets|Delete file') }}</gl-button
+ >
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 76c5779f3ae..fd40c51fec1 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -71,7 +71,7 @@ export default {
</template>
</blob-filepath>
- <div class="file-actions d-none d-sm-flex">
+ <div class="gl-display-none gl-display-sm-flex">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" />
<slot name="actions"></slot>
diff --git a/app/assets/javascripts/blob/components/blob_header_default_actions.vue b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
index 62fef108b47..daade611651 100644
--- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue
+++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlButtonGroup, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import {
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
@@ -10,9 +10,8 @@ import {
export default {
components: {
- GlIcon,
GlButtonGroup,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -48,7 +47,7 @@ export default {
</script>
<template>
<gl-button-group>
- <gl-deprecated-button
+ <gl-button
v-if="!hasRenderError"
v-gl-tooltip.hover
:aria-label="$options.BTN_COPY_CONTENTS_TITLE"
@@ -56,26 +55,29 @@ export default {
:disabled="copyDisabled"
data-clipboard-target="#blob-code-content"
data-testid="copyContentsButton"
- >
- <gl-icon name="copy-to-clipboard" :size="14" />
- </gl-deprecated-button>
- <gl-deprecated-button
+ icon="copy-to-clipboard"
+ category="primary"
+ variant="default"
+ />
+ <gl-button
v-gl-tooltip.hover
:aria-label="$options.BTN_RAW_TITLE"
:title="$options.BTN_RAW_TITLE"
:href="rawPath"
target="_blank"
- >
- <gl-icon name="doc-code" :size="14" />
- </gl-deprecated-button>
- <gl-deprecated-button
+ icon="doc-code"
+ category="primary"
+ variant="default"
+ />
+ <gl-button
v-gl-tooltip.hover
:aria-label="$options.BTN_DOWNLOAD_TITLE"
:title="$options.BTN_DOWNLOAD_TITLE"
:href="downloadUrl"
target="_blank"
- >
- <gl-icon name="download" :size="14" />
- </gl-deprecated-button>
+ icon="download"
+ category="primary"
+ variant="default"
+ />
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
index 5b15fe2d7cc..902dd0b8eec 100644
--- a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
+++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlButtonGroup, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import {
RICH_BLOB_VIEWER,
RICH_BLOB_VIEWER_TITLE,
@@ -9,7 +9,6 @@ import {
export default {
components: {
- GlIcon,
GlButtonGroup,
GlButton,
},
@@ -52,19 +51,21 @@ export default {
:title="$options.SIMPLE_BLOB_VIEWER_TITLE"
:selected="isSimpleViewer"
:class="{ active: isSimpleViewer }"
+ icon="code"
+ category="primary"
+ variant="default"
@click="switchToViewer($options.SIMPLE_BLOB_VIEWER)"
- >
- <gl-icon name="code" :size="14" />
- </gl-button>
+ />
<gl-button
v-gl-tooltip.hover
:aria-label="$options.RICH_BLOB_VIEWER_TITLE"
:title="$options.RICH_BLOB_VIEWER_TITLE"
:selected="isRichViewer"
:class="{ active: isRichViewer }"
+ icon="document"
+ category="primary"
+ variant="default"
@click="switchToViewer($options.RICH_BLOB_VIEWER)"
- >
- <gl-icon name="document" :size="14" />
- </gl-button>
+ />
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index d2c0ef330e4..4409d7a33cc 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -1,12 +1,13 @@
import $ from 'jquery';
import Api from '~/api';
-import Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import FileTemplateTypeSelector from './template_selectors/type_selector';
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector';
+import MetricsDashboardSelector from './template_selectors/metrics_dashboard_selector';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
@@ -30,6 +31,7 @@ export default class FileTemplateMediator {
this.templateSelectors = [
GitignoreSelector,
BlobCiYamlSelector,
+ MetricsDashboardSelector,
DockerfileSelector,
LicenseSelector,
].map(TemplateSelectorClass => new TemplateSelectorClass({ mediator: this }));
diff --git a/app/assets/javascripts/blob/notebook/notebook_viewer.vue b/app/assets/javascripts/blob/notebook/notebook_viewer.vue
index b1713989997..ea33d621d47 100644
--- a/app/assets/javascripts/blob/notebook/notebook_viewer.vue
+++ b/app/assets/javascripts/blob/notebook/notebook_viewer.vue
@@ -1,7 +1,7 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import notebookLab from '~/notebook/index.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index a6f28de799f..12cc2be8246 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 flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
export default () => {
diff --git a/app/assets/javascripts/blob/pdf/pdf_viewer.vue b/app/assets/javascripts/blob/pdf/pdf_viewer.vue
index 64fc832ee54..96d6f500960 100644
--- a/app/assets/javascripts/blob/pdf/pdf_viewer.vue
+++ b/app/assets/javascripts/blob/pdf/pdf_viewer.vue
@@ -1,6 +1,6 @@
<script>
-import PdfLab from '../../pdf/index.vue';
import { GlLoadingIcon } from '@gitlab/ui';
+import PdfLab from '../../pdf/index.vue';
export default {
components: {
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index 3ccd84037a7..90eafb75758 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
-import { sprintf, s__, __ } from '~/locale';
import Cookies from 'js-cookie';
+import { sprintf, s__, __ } from '~/locale';
import { glEmojiTag } from '~/emoji';
import Tracking from '~/tracking';
@@ -11,8 +11,12 @@ export default {
beginnerLink:
'https://about.gitlab.com/blog/2018/01/22/a-beginners-guide-to-continuous-integration/',
exampleLink: 'https://docs.gitlab.com/ee/ci/examples/',
+ codeQualityLink: 'https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html',
bodyMessage: s__(
- 'MR widget|The pipeline will now run automatically every time you commit code. Pipelines are useful for deploying static web pages, detecting vulnerabilities in dependencies, static or dynamic application security testing (SAST and DAST), and so much more!',
+ `MR widget|The pipeline will test your code on every commit. A %{codeQualityLinkStart}code quality report%{codeQualityLinkEnd} will appear in your merge requests to warn you about potential code degradations.`,
+ ),
+ helpMessage: s__(
+ `MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Continuous Integration%{beginnerLinkEnd} and our %{exampleLinkStart}examples of GitLab CI/CD%{exampleLinkEnd} to learn more.`,
),
modalTitle: sprintf(
__("That's it, well done!%{celebrate}"),
@@ -75,15 +79,15 @@ export default {
modal-id="success-pipeline-modal-id-not-used"
>
<p>
- {{ $options.bodyMessage }}
+ <gl-sprintf :message="$options.bodyMessage">
+ <template #codeQualityLink="{content}">
+ <gl-link :href="$options.codeQualityLink" target="_blank" class="font-size-inherit">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
</p>
- <gl-sprintf
- :message="
- s__(`MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Continuous Integration%{beginnerLinkEnd}
- and our %{exampleLinkStart}examples of GitLab CI/CD%{exampleLinkEnd}
- to see all the cool stuff you can do with it.`)
- "
- >
+ <gl-sprintf :message="$options.helpMessage">
<template #beginnerLink="{content}">
<gl-link :href="$options.beginnerLink" target="_blank">
{{ content }}
@@ -105,7 +109,7 @@ export default {
:data-track-event="$options.trackEvent"
:data-track-label="trackLabel"
>
- {{ __('Go to Pipelines') }}
+ {{ __('See your pipeline in action') }}
</a>
</template>
</gl-modal>
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
index 932b6e8a0f7..aff6a56cb0b 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -1,5 +1,5 @@
<script>
-import { GlPopover, GlSprintf, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
+import { GlPopover, GlSprintf, GlButton } from '@gitlab/ui';
import { parseBoolean, scrollToElement, setCookie, getCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -29,8 +29,7 @@ export default {
components: {
GlPopover,
GlSprintf,
- GlIcon,
- GlDeprecatedButton,
+ GlButton,
},
mixins: [trackingMixin],
props: {
@@ -112,18 +111,17 @@ export default {
<template #title>
<span v-html="suggestTitle"></span>
<span class="ml-auto">
- <gl-deprecated-button
+ <gl-button
:aria-label="__('Close')"
class="btn-blank"
name="dismiss"
+ icon="close"
:data-track-property="humanAccess"
:data-track-value="$options.dismissTrackValue"
:data-track-event="$options.clickTrackValue"
:data-track-label="trackLabel"
@click="onDismiss"
- >
- <gl-icon name="close" aria-hidden="true" />
- </gl-deprecated-button>
+ />
</span>
</template>
diff --git a/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
new file mode 100644
index 00000000000..b4accaadfa3
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/metrics_dashboard_selector.js
@@ -0,0 +1,28 @@
+import FileTemplateSelector from '../file_template_selector';
+
+export default class MetricsDashboardSelector extends FileTemplateSelector {
+ constructor({ mediator }) {
+ super(mediator);
+ this.config = {
+ key: 'metrics-dashboard-yaml',
+ name: '.metrics-dashboard.yml',
+ pattern: /(.metrics-dashboard.yml)/,
+ type: 'metrics_dashboard_ymls',
+ dropdown: '.js-metrics-dashboard-selector',
+ wrapper: '.js-metrics-dashboard-selector-wrap',
+ };
+ }
+
+ initDropdown() {
+ this.$dropdown.glDropdown({
+ data: this.$dropdown.data('data'),
+ filterable: true,
+ selectable: true,
+ search: {
+ fields: ['name'],
+ },
+ clicked: options => this.reportSelectionName(options),
+ text: item => item.name,
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js
index 840a3dbe450..a0211c8bb8e 100644
--- a/app/assets/javascripts/blob/utils.js
+++ b/app/assets/javascripts/blob/utils.js
@@ -1,14 +1,17 @@
import Editor from '~/editor/editor_lite';
-export function initEditorLite({ el, blobPath, blobContent }) {
+export function initEditorLite({ el, ...args }) {
if (!el) {
throw new Error(`"el" parameter is required to initialize Editor`);
}
- const editor = new Editor();
+ const editor = new Editor({
+ scrollbar: {
+ alwaysConsumeMouseWheel: false,
+ },
+ });
editor.createInstance({
el,
- blobPath,
- blobContent,
+ ...args,
});
return editor;
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index b18faea628a..05ee8e49eb1 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 Flash from '../../flash';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import { handleLocationHash } from '../../lib/utils/common_utils';
import axios from '../../lib/utils/axios_utils';
import eventHub from '../../notes/event_hub';
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 7e5be8454fe..e22c9b0d4c4 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
import TemplateSelectorMediator from '../blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils';
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 3178bda93b8..384a386d69c 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,7 +1,28 @@
+import ListIssue from 'ee_else_ce/boards/models/issue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
export function getMilestone() {
return null;
}
+export function formatListIssues(listIssues) {
+ return listIssues.nodes.reduce((map, list) => {
+ return {
+ ...map,
+ [list.id]: list.issues.nodes.map(
+ i =>
+ new ListIssue({
+ ...i,
+ id: getIdFromGraphQLId(i.id),
+ labels: i.labels?.nodes || [],
+ assignees: i.assignees?.nodes || [],
+ }),
+ ),
+ };
+ }, {});
+}
+
export default {
getMilestone,
+ formatListIssues,
};
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 0ed7579e8e1..dae24338e45 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -1,10 +1,10 @@
<script>
import Sortable from 'sortablejs';
import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits';
+import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import Tooltip from '~/vue_shared/directives/tooltip';
import EmptyComponent from '~/vue_shared/components/empty_component';
import BoardBlankState from './board_blank_state.vue';
-import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import BoardList from './board_list.vue';
import boardsStore from '../stores/boards_store';
import eventHub from '../eventhub';
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 6ac7fdce6a7..c42295792f1 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,8 +1,8 @@
<script>
import { mapState } from 'vuex';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BoardColumn from 'ee_else_ce/boards/components/board_column.vue';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -42,7 +42,7 @@ export default {
},
},
computed: {
- ...mapState(['isShowingEpicsSwimlanes']),
+ ...mapState(['isShowingEpicsSwimlanes', 'boardLists']),
isSwimlanesOn() {
return this.glFeatures.boardsWithSwimlanes && this.isShowingEpicsSwimlanes;
},
@@ -73,11 +73,12 @@ export default {
<epics-swimlanes
v-else
ref="swimlanes"
- :lists="lists"
+ :lists="boardLists"
:can-admin-list="canAdminList"
:disabled="disabled"
:board-id="boardId"
:group-id="groupId"
+ :root-path="rootPath"
/>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index fbe221041c1..231059b895e 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,6 +1,6 @@
<script>
import { __ } from '~/locale';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import boardsStore from '~/boards/stores/boards_store';
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 4270ad5783d..1a26782f6f0 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,7 +6,7 @@ import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
getBoardSortableDefaultOptions,
sortableStart,
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 02a04cb4e46..bafe07afb48 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -241,7 +241,7 @@ export default {
v-if="isSwimlanesHeader && !list.isExpanded"
ref="collapsedInfo"
aria-hidden="true"
- class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-700"
+ class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-500"
>
<gl-icon name="information" />
</span>
@@ -282,7 +282,7 @@ export default {
<div
v-if="showBoardListAndBoardInfo"
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary"
- :class="{ 'gl-display-none': !list.isExpanded && isSwimlanesHeader }"
+ :class="{ 'gl-display-none!': !list.isExpanded && isSwimlanesHeader }"
>
<span class="gl-display-inline-flex">
<gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" />
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 02ac45f8ef9..34e8438ba4c 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -119,7 +119,7 @@ export default {
autocomplete="off"
/>
<project-select v-if="groupId" :group-id="groupId" :list="list" />
- <div class="clearfix prepend-top-10">
+ <div class="clearfix gl-mt-3">
<gl-button
ref="submit-button"
:disabled="disabled"
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
new file mode 100644
index 00000000000..3149762ecdf
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -0,0 +1,92 @@
+<script>
+import { GlDrawer, GlLabel } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { __ } from '~/locale';
+import boardsStore from '~/boards/stores/boards_store';
+import eventHub from '~/sidebar/event_hub';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import { inactiveId } from '~/boards/constants';
+
+// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
+export default {
+ headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px',
+ listSettingsText: __('List settings'),
+ assignee: 'assignee',
+ milestone: 'milestone',
+ label: 'label',
+ labelListText: __('Label'),
+ components: {
+ GlDrawer,
+ GlLabel,
+ BoardSettingsSidebarWipLimit: () =>
+ import('ee_component/boards/components/board_settings_wip_limit.vue'),
+ BoardSettingsListTypes: () =>
+ import('ee_component/boards/components/board_settings_list_types.vue'),
+ },
+ computed: {
+ ...mapState(['activeId']),
+ activeList() {
+ /*
+ Warning: Though a computed property it is not reactive because we are
+ referencing a List Model class. Reactivity only applies to plain JS objects
+ */
+ return boardsStore.state.lists.find(({ id }) => id === this.activeId);
+ },
+ isSidebarOpen() {
+ return this.activeId !== inactiveId;
+ },
+ activeListLabel() {
+ return this.activeList.label;
+ },
+ boardListType() {
+ return this.activeList.type || null;
+ },
+ listTypeTitle() {
+ return this.$options.labelListText;
+ },
+ },
+ created() {
+ eventHub.$on('sidebar.closeAll', this.closeSidebar);
+ },
+ beforeDestroy() {
+ eventHub.$off('sidebar.closeAll', this.closeSidebar);
+ },
+ methods: {
+ ...mapActions(['setActiveId']),
+ closeSidebar() {
+ this.setActiveId(inactiveId);
+ },
+ showScopedLabels(label) {
+ return boardsStore.scopedLabels.enabled && isScopedLabel(label);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-drawer
+ class="js-board-settings-sidebar"
+ :open="isSidebarOpen"
+ :header-height="$options.headerHeight"
+ @close="closeSidebar"
+ >
+ <template #header>{{ $options.listSettingsText }}</template>
+ <template v-if="isSidebarOpen">
+ <div v-if="boardListType === $options.label">
+ <label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
+ <gl-label
+ :title="activeListLabel.title"
+ :background-color="activeListLabel.color"
+ :scoped="showScopedLabels(activeListLabel)"
+ />
+ </div>
+
+ <board-settings-list-types
+ v-else
+ :active-list="activeList"
+ :board-list-type="boardListType"
+ />
+ <board-settings-sidebar-wip-limit :max-issue-count="activeList.maxIssueCount" />
+ </template>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 056a7b48212..3790c494085 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import Vue from 'vue';
import { GlLabel } from '@gitlab/ui';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { sprintf, __ } from '~/locale';
import Sidebar from '~/right_sidebar';
import eventHub from '~/sidebar/event_hub';
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index dbe3e0790f6..48f6ba6cfc7 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -3,10 +3,10 @@ import { throttle } from 'lodash';
import {
GlLoadingIcon,
GlSearchBoxByType,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownHeader,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownDivider,
+ GlDeprecatedDropdownHeader,
+ GlDeprecatedDropdownItem,
} from '@gitlab/ui';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -26,10 +26,10 @@ export default {
BoardForm,
GlLoadingIcon,
GlSearchBoxByType,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownHeader,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownDivider,
+ GlDeprecatedDropdownHeader,
+ GlDeprecatedDropdownItem,
},
props: {
currentBoard: {
@@ -235,7 +235,7 @@ export default {
<template>
<div class="boards-switcher js-boards-selector gl-mr-3">
<span class="boards-selector-wrapper js-boards-selector-wrapper">
- <gl-dropdown
+ <gl-deprecated-dropdown
data-qa-selector="boards_dropdown"
toggle-class="dropdown-menu-toggle js-dropdown-toggle"
menu-class="flex-column dropdown-extended-height"
@@ -248,9 +248,9 @@ export default {
</div>
</div>
- <gl-dropdown-header class="mt-0">
+ <gl-deprecated-dropdown-header class="mt-0">
<gl-search-box-by-type ref="searchBox" v-model="filterTerm" />
- </gl-dropdown-header>
+ </gl-deprecated-dropdown-header>
<div
v-if="!loading"
@@ -259,26 +259,26 @@ export default {
class="dropdown-content flex-fill"
@scroll.passive="throttledSetScrollFade"
>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-show="filteredBoards.length === 0"
class="no-pointer-events text-secondary"
>
{{ s__('IssueBoards|No matching boards found') }}
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
<h6 v-if="showRecentSection" class="dropdown-bold-header my-0">
{{ __('Recent') }}
</h6>
<template v-if="showRecentSection">
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="recentBoard in recentBoards"
:key="`recent-${recentBoard.id}`"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${recentBoard.id}`"
>
{{ recentBoard.name }}
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
</template>
<hr v-if="showRecentSection" class="my-1" />
@@ -287,21 +287,21 @@ export default {
{{ __('All') }}
</h6>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="otherBoard in filteredBoards"
:key="otherBoard.id"
class="js-dropdown-item"
:href="`${boardBaseUrl}/${otherBoard.id}`"
>
{{ otherBoard.name }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="hasMissingBoards" class="small unclickable">
+ </gl-deprecated-dropdown-item>
+ <gl-deprecated-dropdown-item v-if="hasMissingBoards" class="small unclickable">
{{
s__(
'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
)
}}
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
</div>
<div
@@ -313,25 +313,25 @@ export default {
<gl-loading-icon v-if="loading" />
<div v-if="canAdminBoard">
- <gl-dropdown-divider />
+ <gl-deprecated-dropdown-divider />
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-if="multipleIssueBoardsAvailable"
data-qa-selector="create_new_board_button"
@click.prevent="showPage('new')"
>
{{ s__('IssueBoards|Create new board') }}
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-if="showDelete"
class="text-danger js-delete-board"
@click.prevent="showPage('delete')"
>
{{ s__('IssueBoards|Delete board') }}
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
</div>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
<board-form
v-if="currentPage"
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index 1d70c635c18..4add5ee646a 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -87,11 +87,7 @@ export default {
<template>
<span>
<span ref="issueDueDate" :class="cssClass" class="board-card-info card-number">
- <icon
- :class="{ 'text-danger': isPastDue }"
- class="board-card-info-icon align-top"
- name="calendar"
- />
+ <icon :class="{ 'text-danger': isPastDue }" class="board-card-info-icon" name="calendar" />
<time :class="{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">{{
body
}}</time>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
index 5c33ba9461c..e8b7689da13 100644
--- a/app/assets/javascripts/boards/components/issue_time_estimate.vue
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -34,10 +34,9 @@ export default {
<template>
<span>
<span ref="issueTimeEstimate" class="board-card-info card-number">
- <icon name="hourglass" class="board-card-info-icon align-top" /><time
- class="board-card-info-text"
- >{{ timeEstimate }}</time
- >
+ <icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{
+ timeEstimate
+ }}</time>
</span>
<gl-tooltip
:target="() => $refs.issueTimeEstimate"
diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue
index 5f100c617a0..c4953dda793 100644
--- a/app/assets/javascripts/boards/components/modal/footer.vue
+++ b/app/assets/javascripts/boards/components/modal/footer.vue
@@ -1,6 +1,6 @@
<script>
import footerEEMixin from 'ee_else_ce/boards/mixins/modal_footer';
-import Flash from '../../../flash';
+import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __, n__ } from '../../../locale';
import ListsDropdown from './lists_dropdown.vue';
import ModalStore from '../../stores/modal_store';
diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue
index 8eae8e4726f..573284d2b44 100644
--- a/app/assets/javascripts/boards/components/modal/header.vue
+++ b/app/assets/javascripts/boards/components/modal/header.vue
@@ -67,7 +67,7 @@ export default {
</h2>
</header>
<modal-tabs v-if="!loading && issuesCount > 0" />
- <div v-if="showSearch" class="d-flex append-bottom-10">
+ <div v-if="showSearch" class="d-flex gl-mb-3">
<modal-filters :store="filter" />
<button
ref="selectAllBtn"
diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue
index ed67206218e..a71fda9d7c5 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.vue
+++ b/app/assets/javascripts/boards/components/modal/tabs.vue
@@ -19,7 +19,7 @@ export default {
};
</script>
<template>
- <div class="top-area prepend-top-10 append-bottom-10">
+ <div class="top-area gl-mt-3 gl-mb-3">
<ul class="nav-links issues-state-filters">
<li :class="{ active: activeTab == 'all' }">
<a href="#" role="button" @click.prevent="changeTab('all')">
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 229bb82152b..2b9fdf11b37 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import CreateLabelDropdown from '../../create_label';
import boardsStore from '../stores/boards_store';
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 8fd377938b4..598e92726c1 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -64,10 +64,10 @@ export default {
this.groupId,
term,
{
- search_namespaces: true,
with_issues_enabled: true,
with_shared: false,
include_subgroups: true,
+ order_by: 'similarity',
...additionalAttrs,
},
projects => {
@@ -97,7 +97,7 @@ export default {
<template>
<div>
- <label class="label-bold prepend-top-10">{{ __('Project') }}</label>
+ <label class="label-bold gl-mt-3">{{ __('Project') }}</label>
<div ref="projectsDropdown" class="dropdown dropdown-projects">
<button
class="dropdown-menu-toggle wide"
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
index 71e5d8058da..4e5a6609042 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -1,10 +1,14 @@
<script>
+import { GlButton } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
-import Flash from '../../../flash';
+import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __ } from '../../../locale';
import boardsStore from '../../stores/boards_store';
export default {
+ components: {
+ GlButton,
+ },
props: {
issue: {
type: Object,
@@ -75,8 +79,8 @@ export default {
</script>
<template>
<div class="block list">
- <button class="btn btn-default btn-block" type="button" @click="removeIssue">
+ <gl-button variant="default" category="secondary" block="block" @click="removeIssue">
{{ __('Remove from board') }}
- </button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index f577a168e75..35c52558cac 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -13,7 +13,7 @@ export const ListType = {
blank: 'blank',
};
-export const inactiveListId = 0;
+export const inactiveId = 0;
export default {
BoardType,
diff --git a/app/assets/javascripts/boards/eventhub.js b/app/assets/javascripts/boards/eventhub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/boards/eventhub.js
+++ b/app/assets/javascripts/boards/eventhub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index ca85e54eb89..b7966dd869d 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,6 +1,6 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
-import FilteredSearchContainer from '../filtered_search/container';
import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
+import FilteredSearchContainer from '../filtered_search/container';
import boardsStore from './stores/boards_store';
export default class FilteredSearchBoards extends FilteredSearchManager {
@@ -10,6 +10,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
isGroupDecendent: true,
stateFiltersSelector: '.issues-state-filters',
isGroup: IS_EE,
+ useDefaultState: false,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 5b4a1d262dd..971edd71eec 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -4,7 +4,6 @@ import { mapActions } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
-import BoardContent from '~/boards/components/board_content.vue';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
@@ -19,8 +18,9 @@ import {
} from 'ee_else_ce/boards/ee_functions';
import VueApollo from 'vue-apollo';
+import BoardContent from '~/boards/components/board_content.vue';
import createDefaultClient from '~/lib/graphql';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
import './models/label';
import './models/assignee';
@@ -83,8 +83,7 @@ export default () => {
Board: () => import('ee_else_ce/boards/components/board_column.vue'),
BoardSidebar,
BoardAddIssuesModal,
- BoardSettingsSidebar: () =>
- import('ee_component/boards/components/board_settings_sidebar.vue'),
+ BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
},
store,
apolloProvider,
@@ -118,7 +117,7 @@ export default () => {
boardId: this.boardId,
fullPath: $boardApp.dataset.fullPath,
};
- this.setEndpoints(endpoints);
+ this.setInitialBoardData({ ...endpoints, boardType: this.parent });
boardsStore.setEndpoints(endpoints);
boardsStore.rootPath = this.boardsEndpoint;
@@ -190,7 +189,7 @@ export default () => {
}
},
methods: {
- ...mapActions(['setEndpoints']),
+ ...mapActions(['setInitialBoardData']),
updateTokens() {
this.filterManager.updateTokens();
},
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 2aa92f86125..b8b30c958a9 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,12 +1,11 @@
/* eslint-disable no-underscore-dangle, class-methods-use-this */
-
-import ListIssue from 'ee_else_ce/boards/models/issue';
import { __ } from '~/locale';
import ListLabel from './label';
import ListAssignee from './assignee';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import boardsStore from '../stores/boards_store';
import ListMilestone from './milestone';
+import 'ee_else_ce/boards/models/issue';
const TYPES = {
backlog: {
@@ -61,7 +60,9 @@ class List {
this.title = this.milestone.title;
}
- if (!typeInfo.isBlank && this.id) {
+ // doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards
+ // Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/229416
+ if (!typeInfo.isBlank && this.id && !obj.doNotFetchIssues) {
this.getIssues().catch(() => {
// TODO: handle request error
});
@@ -100,12 +101,6 @@ class List {
return boardsStore.newListIssue(this, issue);
}
- createIssues(data) {
- data.forEach(issueObj => {
- this.addIssue(new ListIssue(issueObj));
- });
- }
-
addMultipleIssues(issues, listFrom, newIndex) {
boardsStore.addMultipleListIssues(this, issues, listFrom, newIndex);
}
diff --git a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
index 5b532906f6a..8abd79332fb 100644
--- a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
+++ b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
@@ -4,6 +4,7 @@ fragment BoardListShared on BoardList {
position
listType
collapsed
+ maxIssueCount
label {
id
title
diff --git a/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql b/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql
new file mode 100644
index 00000000000..724c7884c58
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/group_lists_issues.query.graphql
@@ -0,0 +1,18 @@
+#import "./issue.fragment.graphql"
+
+query GroupListIssues($fullPath: ID!, $boardId: ID!) {
+ group(fullPath: $fullPath) {
+ board(id: $boardId) {
+ lists {
+ nodes {
+ id
+ issues {
+ nodes {
+ ...IssueNode
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/issue.fragment.graphql b/app/assets/javascripts/boards/queries/issue.fragment.graphql
new file mode 100644
index 00000000000..89d56b895a4
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/issue.fragment.graphql
@@ -0,0 +1,31 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+fragment IssueNode on Issue {
+ id
+ iid
+ title
+ referencePath: reference(full: true)
+ dueDate
+ timeEstimate
+ weight
+ confidential
+ webUrl
+ subscribed
+ blocked
+ epic {
+ id
+ }
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ labels {
+ nodes {
+ id
+ title
+ color
+ description
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql b/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql
new file mode 100644
index 00000000000..149b76848ef
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/project_lists_issues.query.graphql
@@ -0,0 +1,18 @@
+#import "./issue.fragment.graphql"
+
+query ProjectListIssues($fullPath: ID!, $boardId: ID!) {
+ project(fullPath: $fullPath) {
+ board(id: $boardId) {
+ lists {
+ nodes {
+ id
+ issues {
+ nodes {
+ ...IssueNode
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 08fedb14dff..b4be7546252 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,4 +1,11 @@
import * as types from './mutation_types';
+import createDefaultClient from '~/lib/graphql';
+import { BoardType } from '~/boards/constants';
+import { formatListIssues } from '../boards_util';
+import groupListsIssuesQuery from '../queries/group_lists_issues.query.graphql';
+import projectListsIssuesQuery from '../queries/project_lists_issues.query.graphql';
+
+const gqlClient = createDefaultClient();
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -6,8 +13,12 @@ const notImplemented = () => {
};
export default {
- setEndpoints: ({ commit }, endpoints) => {
- commit(types.SET_ENDPOINTS, endpoints);
+ setInitialBoardData: ({ commit }, data) => {
+ commit(types.SET_INITIAL_BOARD_DATA, data);
+ },
+
+ setActiveId({ commit }, id) {
+ commit(types.SET_ACTIVE_ID, id);
},
fetchLists: () => {
@@ -34,6 +45,32 @@ export default {
notImplemented();
},
+ fetchIssuesForAllLists: ({ state, commit }) => {
+ commit(types.REQUEST_ISSUES_FOR_ALL_LISTS);
+
+ const { endpoints, boardType } = state;
+ const { fullPath, boardId } = endpoints;
+
+ const query = boardType === BoardType.group ? groupListsIssuesQuery : projectListsIssuesQuery;
+
+ const variables = {
+ fullPath,
+ boardId: `gid://gitlab/Board/${boardId}`,
+ };
+
+ return gqlClient
+ .query({
+ query,
+ variables,
+ })
+ .then(({ data }) => {
+ const { lists } = data[boardType]?.board;
+ const listIssues = formatListIssues(lists);
+ commit(types.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS, listIssues);
+ })
+ .catch(() => commit(types.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE));
+ },
+
moveIssue: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index da7d2e19ec1..30c71d64085 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -1,6 +1,6 @@
/* eslint-disable no-shadow, no-param-reassign,consistent-return */
/* global List */
-
+/* global ListIssue */
import $ from 'jquery';
import { sortBy } from 'lodash';
import Vue from 'vue';
@@ -81,7 +81,7 @@ const boardsStore = {
showPage(page) {
this.state.currentPage = page;
},
- addList(listObj) {
+ updateListPosition(listObj) {
const listType = listObj.listType || listObj.list_type;
let { position } = listObj;
if (listType === ListType.closed) {
@@ -91,6 +91,10 @@ const boardsStore = {
}
const list = new List({ ...listObj, position });
+ return list;
+ },
+ addList(listObj) {
+ const list = this.updateListPosition(listObj);
this.state.lists = sortBy([...this.state.lists, list], 'position');
return list;
},
@@ -641,7 +645,9 @@ const boardsStore = {
list.issues = [];
}
- list.createIssues(data.issues);
+ data.issues.forEach(issueObj => {
+ list.addIssue(new ListIssue(issueObj));
+ });
return data;
});
@@ -848,19 +854,28 @@ const boardsStore = {
},
refreshIssueData(issue, obj) {
- issue.id = obj.id;
- issue.iid = obj.iid;
- issue.title = obj.title;
- issue.confidential = obj.confidential;
- issue.dueDate = obj.due_date;
- issue.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
- issue.referencePath = obj.reference_path;
- issue.path = obj.real_path;
- issue.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
+ // issue.id = obj.id;
+ // issue.iid = obj.iid;
+ // issue.title = obj.title;
+ // issue.confidential = obj.confidential;
+ // issue.dueDate = obj.due_date || obj.dueDate;
+ // issue.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
+ // issue.referencePath = obj.reference_path || obj.referencePath;
+ // issue.path = obj.real_path || obj.webUrl;
+ // issue.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
+ // issue.project_id = obj.project_id;
+ // issue.timeEstimate = obj.time_estimate || obj.timeEstimate;
+ // issue.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
+ // issue.blocked = obj.blocked;
+ // issue.epic = obj.epic;
+
+ const convertedObj = convertObjectPropsToCamelCase(obj, {
+ dropKeys: ['issue_sidebar_endpoint', 'real_path', 'webUrl'],
+ });
+ convertedObj.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
+ issue.path = obj.real_path || obj.webUrl;
issue.project_id = obj.project_id;
- issue.timeEstimate = obj.time_estimate;
- issue.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
- issue.blocked = obj.blocked;
+ Object.assign(issue, convertedObj);
if (obj.project) {
issue.project = new IssueProject(obj.project);
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index fcdfa6799b6..0f96dc2e287 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -1,4 +1,4 @@
-export const SET_ENDPOINTS = 'SET_ENDPOINTS';
+export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST';
export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
@@ -8,6 +8,9 @@ export const RECEIVE_UPDATE_LIST_ERROR = 'RECEIVE_UPDATE_LIST_ERROR';
export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST';
export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS';
export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR';
+export const REQUEST_ISSUES_FOR_ALL_LISTS = 'REQUEST_ISSUES_FOR_ALL_LISTS';
+export const RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS = 'RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS';
+export const RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE = 'RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE';
export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE';
export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS';
export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR';
@@ -19,3 +22,4 @@ export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
+export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index e4459cdcc07..ca9b911ce5b 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -6,8 +6,14 @@ const notImplemented = () => {
};
export default {
- [mutationTypes.SET_ENDPOINTS]: (state, endpoints) => {
+ [mutationTypes.SET_INITIAL_BOARD_DATA]: (state, data) => {
+ const { boardType, ...endpoints } = data;
state.endpoints = endpoints;
+ state.boardType = boardType;
+ },
+
+ [mutationTypes.SET_ACTIVE_ID](state, id) {
+ state.activeId = id;
},
[mutationTypes.REQUEST_ADD_LIST]: () => {
@@ -46,6 +52,20 @@ export default {
notImplemented();
},
+ [mutationTypes.REQUEST_ISSUES_FOR_ALL_LISTS]: state => {
+ state.isLoadingIssues = true;
+ },
+
+ [mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS]: (state, listIssues) => {
+ state.issuesByListId = listIssues;
+ state.isLoadingIssues = false;
+ },
+
+ [mutationTypes.RECEIVE_ISSUES_FOR_ALL_LISTS_FAILURE]: state => {
+ state.listIssueFetchFailure = true;
+ state.isLoadingIssues = false;
+ },
+
[mutationTypes.REQUEST_ADD_ISSUE]: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index aca93c4d7c6..cb6930774ed 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -1,7 +1,11 @@
-import { inactiveListId } from '~/boards/constants';
+import { inactiveId } from '~/boards/constants';
export default () => ({
endpoints: {},
+ boardType: null,
isShowingLabels: true,
- activeListId: inactiveListId,
+ activeId: inactiveId,
+ issuesByListId: {},
+ isLoadingIssues: false,
+ listIssueFetchFailure: false,
});
diff --git a/app/assets/javascripts/branches/components/divergence_graph.vue b/app/assets/javascripts/branches/components/divergence_graph.vue
index 36fff370ea1..deaed694b46 100644
--- a/app/assets/javascripts/branches/components/divergence_graph.vue
+++ b/app/assets/javascripts/branches/components/divergence_graph.vue
@@ -65,7 +65,7 @@ export default {
</template>
<template v-else>
<graph-bar :count="behindCount" :max-commits="maxCommits" position="left" />
- <div class="graph-separator pull-left mt-1"></div>
+ <div class="graph-separator float-left mt-1"></div>
<graph-bar :count="aheadCount" :max-commits="maxCommits" position="right" />
</template>
</div>
diff --git a/app/assets/javascripts/branches/components/graph_bar.vue b/app/assets/javascripts/branches/components/graph_bar.vue
index 83da41ca097..21cbcac820a 100644
--- a/app/assets/javascripts/branches/components/graph_bar.vue
+++ b/app/assets/javascripts/branches/components/graph_bar.vue
@@ -56,7 +56,7 @@ export default {
</script>
<template>
- <div :class="{ full: isFullWidth }" class="position-relative pull-left pt-1 graph-side h-100">
+ <div :class="{ full: isFullWidth }" class="position-relative float-left pt-1 graph-side h-100">
<div
:style="style"
:class="[roundedClass, positionSideClass]"
diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js
index 303735a1807..89e9d3fcb62 100644
--- a/app/assets/javascripts/branches/divergence_graph.js
+++ b/app/assets/javascripts/branches/divergence_graph.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { __ } from '../locale';
-import createFlash from '../flash';
+import { deprecatedCreateFlash as createFlash } from '../flash';
import axios from '../lib/utils/axios_utils';
import DivergenceGraph from './components/divergence_graph.vue';
diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
index 470649e63fb..b8bf363fc9d 100644
--- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
@@ -1,7 +1,7 @@
import { escape } from 'lodash';
import axios from '../lib/utils/axios_utils';
import { s__ } from '../locale';
-import Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import { parseBoolean } from '../lib/utils/common_utils';
import statusCodes from '../lib/utils/http_status';
import VariableList from './ci_variable_list';
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
index 175e89a454b..d22fef27964 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
@@ -1,20 +1,20 @@
<script>
import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
+ GlDeprecatedDropdownDivider,
GlSearchBoxByType,
GlIcon,
} from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
import { mapGetters } from 'vuex';
+import { __, sprintf } from '~/locale';
export default {
name: 'CiEnvironmentsDropdown',
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
+ GlDeprecatedDropdownDivider,
GlSearchBoxByType,
GlIcon,
},
@@ -66,9 +66,9 @@ export default {
};
</script>
<template>
- <gl-dropdown :text="value">
+ <gl-deprecated-dropdown :text="value">
<gl-search-box-by-type v-model.trim="searchTerm" class="m-2" />
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="environment in filteredResults"
:key="environment"
@click="selectEnvironment(environment)"
@@ -79,15 +79,15 @@ export default {
class="vertical-align-middle"
/>
{{ environment }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{
+ </gl-deprecated-dropdown-item>
+ <gl-deprecated-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{
__('No matching results')
- }}</gl-dropdown-item>
+ }}</gl-deprecated-dropdown-item>
<template v-if="shouldRenderCreateButton">
- <gl-dropdown-divider />
- <gl-dropdown-item @click="createClicked">
+ <gl-deprecated-dropdown-divider />
+ <gl-deprecated-dropdown-item @click="createClicked">
{{ composedCreateButtonLabel }}
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
</template>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index 0ba58430de1..fbf19847e9d 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -3,7 +3,6 @@ import {
GlAlert,
GlButton,
GlCollapse,
- GlDeprecatedButton,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
@@ -39,7 +38,6 @@ export default {
GlAlert,
GlButton,
GlCollapse,
- GlDeprecatedButton,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
@@ -210,6 +208,7 @@ export default {
v-model="key"
:token-list="$options.tokenList"
:label-text="__('Key')"
+ data-qa-selector="ci_variable_key_field"
/>
<gl-form-group v-else :label="__('Key')" label-for="ci-variable-key">
@@ -242,7 +241,7 @@ export default {
<gl-form-group
:label="__('Type')"
label-for="ci-variable-type"
- class="w-50 append-right-15"
+ class="w-50 gl-mr-5"
:class="{ 'w-100': isGroup }"
>
<gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" />
@@ -339,24 +338,25 @@ export default {
</gl-alert>
</gl-collapse>
<template #modal-footer>
- <gl-deprecated-button @click="hideModal">{{ __('Cancel') }}</gl-deprecated-button>
- <gl-deprecated-button
+ <gl-button @click="hideModal">{{ __('Cancel') }}</gl-button>
+ <gl-button
v-if="variableBeingEdited"
ref="deleteCiVariable"
- category="secondary"
variant="danger"
+ category="secondary"
data-qa-selector="ci_variable_delete_button"
@click="deleteVarAndClose"
- >{{ __('Delete variable') }}</gl-deprecated-button
+ >{{ __('Delete variable') }}</gl-button
>
- <gl-deprecated-button
+ <gl-button
ref="updateOrAddVariable"
:disabled="!canSubmit"
variant="success"
+ category="primary"
data-qa-selector="ci_variable_save_button"
@click="updateOrAddVariable"
>{{ modalActionText }}
- </gl-deprecated-button>
+ </gl-button>
</template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
index 07b0d55bd4c..431819124c2 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
@@ -1,12 +1,11 @@
<script>
-import { GlPopover, GlIcon, GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
export default {
maxTextLength: 95,
components: {
GlPopover,
- GlIcon,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -39,16 +38,18 @@ export default {
<template>
<div id="popover-container">
<gl-popover :target="target" triggers="hover" placement="top" container="popover-container">
- <div class="d-flex justify-content-between position-relative">
- <div class="pr-5 w-100 ci-popover-value">{{ displayValue }}</div>
- <gl-deprecated-button
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <div class="ci-popover-value gl-pr-3">
+ {{ displayValue }}
+ </div>
+ <gl-button
v-gl-tooltip
- class="btn-transparent btn-clipboard position-absolute position-top-0 position-right-0"
+ category="tertiary"
+ icon="copy-to-clipboard"
:title="tooltipText"
:data-clipboard-text="value"
- >
- <gl-icon name="copy-to-clipboard" />
- </gl-deprecated-button>
+ :aria-label="__('Copy to clipboard')"
+ />
</div>
</gl-popover>
</div>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
index ed1240c247f..12bc5ad3549 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
@@ -1,7 +1,7 @@
<script>
+import { mapState, mapActions } from 'vuex';
import CiVariableModal from './ci_variable_modal.vue';
import CiVariableTable from './ci_variable_table.vue';
-import { mapState, mapActions } from 'vuex';
export default {
components: {
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 7b703c5ede1..018704bff74 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
@@ -1,7 +1,7 @@
<script>
-import { GlTable, GlDeprecatedButton, GlModalDirective, GlIcon } from '@gitlab/ui';
-import { s__, __ } from '~/locale';
+import { GlTable, GlButton, GlModalDirective, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
+import { s__, __ } from '~/locale';
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
import CiVariablePopover from './ci_variable_popover.vue';
@@ -51,7 +51,7 @@ export default {
],
components: {
GlTable,
- GlDeprecatedButton,
+ GlButton,
GlIcon,
CiVariablePopover,
},
@@ -147,14 +147,14 @@ export default {
</div>
</template>
<template #cell(actions)="{ item }">
- <gl-deprecated-button
+ <gl-button
ref="edit-ci-variable"
v-gl-modal-directive="$options.modalId"
+ icon="pencil"
+ :aria-label="__('Edit')"
data-qa-selector="edit_ci_variable_button"
@click="editVariable(item)"
- >
- <gl-icon :size="$options.iconSize" name="pencil" />
- </gl-deprecated-button>
+ />
</template>
<template #empty>
<p ref="empty-variables" class="text-center empty-variables text-plain">
@@ -166,20 +166,21 @@ export default {
class="ci-variable-actions d-flex justify-content-end"
:class="{ 'justify-content-center': !tableIsNotEmpty }"
>
- <gl-deprecated-button
+ <gl-button
v-if="tableIsNotEmpty"
ref="secret-value-reveal-button"
data-qa-selector="reveal_ci_variable_value_button"
class="gl-mr-3"
@click="toggleValues(!valuesHidden)"
- >{{ valuesButtonText }}</gl-deprecated-button
+ >{{ valuesButtonText }}</gl-button
>
- <gl-deprecated-button
+ <gl-button
ref="add-ci-variable"
v-gl-modal-directive="$options.modalId"
data-qa-selector="add_ci_variable_button"
variant="success"
- >{{ __('Add Variable') }}</gl-deprecated-button
+ category="primary"
+ >{{ __('Add Variable') }}</gl-button
>
</div>
</div>
diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js
index 60c7a480769..e3e9dac0a79 100644
--- a/app/assets/javascripts/ci_variable_list/store/actions.js
+++ b/app/assets/javascripts/ci_variable_list/store/actions.js
@@ -1,7 +1,7 @@
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import { prepareDataForApi, prepareDataForDisplay, prepareEnvironments } from './utils';
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 83bdea15e62..92517203972 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -4,23 +4,15 @@ import { GlToast } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
-import Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
-import {
- APPLICATION_STATUS,
- INGRESS,
- INGRESS_DOMAIN_SUFFIX,
- CROSSPLANE,
- KNATIVE,
- FLUENTD,
-} from './constants';
+import { APPLICATION_STATUS, CROSSPLANE, KNATIVE, FLUENTD } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
-import setupToggleButtons from '../toggle_buttons';
import initProjectSelectDropdown from '~/project_select';
import initServerlessSurveyBanner from '~/serverless/survey_banner';
@@ -68,6 +60,7 @@ export default class Clusters {
deployBoardsHelpPath,
cloudRunHelpPath,
clusterId,
+ ciliumHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.clusterId = clusterId;
@@ -84,6 +77,7 @@ export default class Clusters {
clustersHelpPath,
deployBoardsHelpPath,
cloudRunHelpPath,
+ ciliumHelpPath,
);
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
@@ -119,19 +113,11 @@ export default class Clusters {
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
this.tokenField = document.querySelector('.js-cluster-token');
- this.ingressDomainHelpText = document.querySelector('.js-ingress-domain-help-text');
- this.ingressDomainSnippet =
- this.ingressDomainHelpText &&
- this.ingressDomainHelpText.querySelector('.js-ingress-domain-snippet');
initProjectSelectDropdown();
Clusters.initDismissableCallout();
initSettingsPanels();
- const toggleButtonsContainer = document.querySelector('.js-cluster-enable-toggle-area');
- if (toggleButtonsContainer) {
- setupToggleButtons(toggleButtonsContainer);
- }
this.initApplications(clusterType);
this.initEnvironments();
@@ -184,6 +170,7 @@ export default class Clusters {
providerType: this.state.providerType,
preInstalledKnative: this.state.preInstalledKnative,
rbac: this.state.rbac,
+ ciliumHelpPath: this.state.ciliumHelpPath,
},
});
},
@@ -329,13 +316,6 @@ export default class Clusters {
this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
- if (this.ingressDomainHelpText) {
- this.toggleIngressDomainHelpText(
- prevApplicationMap[INGRESS],
- this.store.state.applications[INGRESS],
- );
- }
-
if (this.store.state.applications[KNATIVE]?.status === APPLICATION_STATUS.INSTALLED) {
initServerlessSurveyBanner();
}
@@ -507,13 +487,6 @@ export default class Clusters {
});
}
- toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) {
- if (externalIp !== newExternalIp) {
- this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp);
- this.ingressDomainSnippet.textContent = `${newExternalIp}${INGRESS_DOMAIN_SUFFIX}`;
- }
- }
-
saveKnativeDomain(data) {
const appId = data.id;
this.store.updateApplication(appId);
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index ba6de41e025..c86db28515f 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -52,6 +52,11 @@ export default {
required: false,
default: false,
},
+ installable: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
uninstallable: {
type: Boolean,
required: false,
@@ -141,6 +146,7 @@ export default {
return (
this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
this.status === APPLICATION_STATUS.INSTALLABLE ||
+ this.status === APPLICATION_STATUS.UNINSTALLED ||
this.isUnknownStatus
);
},
@@ -164,14 +170,20 @@ export default {
return !this.status || this.isInstalling;
},
installButtonDisabled() {
+ // Applications installed through the management project can
+ // only be installed through the CI pipeline. Installation should
+ // be disable in all states.
+ if (!this.installable) return true;
+
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
+ if (this.isInstalling) return true;
+
+ if (!this.isKnownStatus) return false;
+
return (
- ((this.status !== APPLICATION_STATUS.INSTALLABLE &&
- this.status !== APPLICATION_STATUS.ERROR) ||
- this.isInstalling) &&
- this.isKnownStatus
+ this.status !== APPLICATION_STATUS.INSTALLABLE && this.status !== APPLICATION_STATUS.ERROR
);
},
installButtonLabel() {
@@ -335,7 +347,7 @@ export default {
<div>
<slot name="description"></slot>
</div>
- <div v-if="hasError" class="cluster-application-error text-danger prepend-top-10">
+ <div v-if="hasError" class="cluster-application-error text-danger gl-mt-3">
<p class="js-cluster-application-general-error-message gl-mb-0">
{{ generalErrorDescription }}
</p>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 214906021ad..039237042ea 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,8 +1,6 @@
<script>
-import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg';
import { GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
-import helmLogo from 'images/cluster_app_logos/helm.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import certManagerLogo from 'images/cluster_app_logos/cert_manager.png';
@@ -88,18 +86,13 @@ export default {
required: false,
default: false,
},
+ ciliumHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
- managedAppsLocalTillerEnabled() {
- return Boolean(gon.features?.managedAppsLocalTiller);
- },
- helmInstalled() {
- return (
- this.managedAppsLocalTillerEnabled ||
- this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
- this.applications.helm.status === APPLICATION_STATUS.UPDATED
- );
- },
ingressId() {
return INGRESS;
},
@@ -157,7 +150,6 @@ export default {
},
logos: {
gitlabLogo,
- helmLogo,
jupyterhubLogo,
kubernetesLogo,
certManagerLogo,
@@ -167,7 +159,6 @@ export default {
elasticStackLogo,
fluentdLogo,
},
- helmInstallIllustration,
};
</script>
@@ -175,46 +166,12 @@ export default {
<section id="cluster-applications">
<p class="gl-mb-0">
{{
- s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.
- Helm Tiller is required to install any of the following applications.`)
+ s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.`)
}}
<gl-link :href="helpPath">{{ __('More information') }}</gl-link>
</p>
- <div class="cluster-application-list prepend-top-10">
- <application-row
- v-if="!managedAppsLocalTillerEnabled"
- id="helm"
- :logo-url="$options.logos.helmLogo"
- :title="applications.helm.title"
- :status="applications.helm.status"
- :status-reason="applications.helm.statusReason"
- :request-status="applications.helm.requestStatus"
- :request-reason="applications.helm.requestReason"
- :installed="applications.helm.installed"
- :install-failed="applications.helm.installFailed"
- :uninstallable="applications.helm.uninstallable"
- :uninstall-successful="applications.helm.uninstallSuccessful"
- :uninstall-failed="applications.helm.uninstallFailed"
- class="rounded-top"
- title-link="https://docs.helm.sh/"
- >
- <template #description>
- {{
- s__(`ClusterIntegration|Helm streamlines installing
- and managing Kubernetes applications.
- Tiller runs inside of your Kubernetes Cluster,
- and manages releases of your charts.`)
- }}
- </template>
- </application-row>
- <div v-show="!helmInstalled" class="cluster-application-warning">
- <div class="svg-container" v-html="$options.helmInstallIllustration"></div>
- {{
- s__(`ClusterIntegration|You must first install Helm Tiller before
- installing the applications below`)
- }}
- </div>
+ <div class="cluster-application-list gl-mt-3">
<application-row
:id="ingressId"
:logo-url="$options.logos.kubernetesLogo"
@@ -232,7 +189,6 @@ export default {
:uninstallable="applications.ingress.uninstallable"
:uninstall-successful="applications.ingress.uninstallSuccessful"
:uninstall-failed="applications.ingress.uninstallFailed"
- :disabled="!helmInstalled"
:updateable="false"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
@@ -335,7 +291,6 @@ export default {
:uninstallable="applications.cert_manager.uninstallable"
:uninstall-successful="applications.cert_manager.uninstallSuccessful"
:uninstall-failed="applications.cert_manager.uninstallFailed"
- :disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#"
>
<template #description>
@@ -393,7 +348,6 @@ export default {
:uninstallable="applications.prometheus.uninstallable"
:uninstall-successful="applications.prometheus.uninstallSuccessful"
:uninstall-failed="applications.prometheus.uninstallFailed"
- :disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/"
>
<template #description>
@@ -433,7 +387,6 @@ export default {
:uninstallable="applications.runner.uninstallable"
:uninstall-successful="applications.runner.uninstallSuccessful"
:uninstall-failed="applications.runner.uninstallFailed"
- :disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/"
>
<template #description>
@@ -459,7 +412,6 @@ export default {
:uninstall-successful="applications.crossplane.uninstallSuccessful"
:uninstall-failed="applications.crossplane.uninstallFailed"
:install-application-request-params="{ stack: applications.crossplane.stack }"
- :disabled="!helmInstalled"
title-link="https://crossplane.io"
>
<template #description>
@@ -504,7 +456,6 @@ export default {
:uninstall-successful="applications.jupyter.uninstallSuccessful"
:uninstall-failed="applications.jupyter.uninstallFailed"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
- :disabled="!helmInstalled"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
>
<template #description>
@@ -570,7 +521,6 @@ export default {
:uninstall-successful="applications.knative.uninstallSuccessful"
:uninstall-failed="applications.knative.uninstallFailed"
:updateable="false"
- :disabled="!helmInstalled"
v-bind="applications.knative"
title-link="https://github.com/knative/docs"
>
@@ -592,7 +542,7 @@ export default {
</p>
<knative-domain-editor
- v-if="(knative.installed || (helmInstalled && rbac)) && !preInstalledKnative"
+ v-if="(knative.installed || rbac) && !preInstalledKnative"
:knative="knative"
:ingress-dns-help-path="ingressDnsHelpPath"
@save="saveKnativeDomain"
@@ -629,7 +579,6 @@ export default {
:uninstallable="applications.elastic_stack.uninstallable"
:uninstall-successful="applications.elastic_stack.uninstallSuccessful"
:uninstall-failed="applications.elastic_stack.uninstallFailed"
- :disabled="!helmInstalled"
title-link="https://gitlab.com/gitlab-org/charts/elastic-stack"
>
<template #description>
@@ -663,7 +612,6 @@ export default {
:uninstallable="applications.fluentd.uninstallable"
:uninstall-successful="applications.fluentd.uninstallSuccessful"
:uninstall-failed="applications.fluentd.uninstallFailed"
- :disabled="!helmInstalled"
:updateable="false"
title-link="https://github.com/helm/charts/tree/master/stable/fluentd"
>
@@ -687,6 +635,39 @@ export default {
/>
</template>
</application-row>
+
+ <div class="gl-mt-7 gl-border-1 gl-border-t-solid gl-border-gray-100">
+ <!-- This empty div serves as a separator. The applications below can be externally installed using a cluster-management project. -->
+ </div>
+
+ <application-row
+ id="cilium"
+ :title="applications.cilium.title"
+ :logo-url="$options.logos.gitlabLogo"
+ :status="applications.cilium.status"
+ :status-reason="applications.cilium.statusReason"
+ :installable="applications.cilium.installable"
+ :uninstallable="applications.cilium.uninstallable"
+ :installed="applications.cilium.installed"
+ :install-failed="applications.cilium.installFailed"
+ :title-link="ciliumHelpPath"
+ >
+ <template #description>
+ <p data-testid="ciliumDescription">
+ <gl-sprintf
+ :message="
+ s__(
+ 'ClusterIntegration|Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints. %{linkStart}Learn more about configuring Network Policies here.%{linkEnd}',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="ciliumHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </application-row>
</div>
</section>
</template>
diff --git a/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue b/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue
index 6b99bb09504..c816fc56d7a 100644
--- a/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue
+++ b/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue
@@ -1,12 +1,12 @@
<script>
-import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlIcon } from '@gitlab/ui';
import { s__ } from '../../locale';
export default {
name: 'CrossplaneProviderStack',
components: {
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
GlIcon,
},
props: {
@@ -67,17 +67,21 @@ export default {
<label>
{{ s__('ClusterIntegration|Enabled stack') }}
</label>
- <gl-dropdown
+ <gl-deprecated-dropdown
:disabled="crossplane.installed"
:text="dropdownText"
toggle-class="dropdown-menu-toggle gl-field-error-outline"
class="w-100"
:class="{ 'gl-show-field-errors': validationError }"
>
- <gl-dropdown-item v-for="stack in stacks" :key="stack.code" @click="selectStack(stack)">
+ <gl-deprecated-dropdown-item
+ v-for="stack in stacks"
+ :key="stack.code"
+ @click="selectStack(stack)"
+ >
<span class="ml-1">{{ stack.name }}</span>
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
<span v-if="validationError" class="gl-field-error">{{ validationError }}</span>
<p class="form-text text-muted">
{{ s__(`You must select a stack for configuring your cloud provider. Learn more about`) }}
diff --git a/app/assets/javascripts/clusters/components/fluentd_output_settings.vue b/app/assets/javascripts/clusters/components/fluentd_output_settings.vue
index 20f6210aba8..e6001b11296 100644
--- a/app/assets/javascripts/clusters/components/fluentd_output_settings.vue
+++ b/app/assets/javascripts/clusters/components/fluentd_output_settings.vue
@@ -1,15 +1,15 @@
<script>
-import { __ } from '~/locale';
-import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
import {
GlAlert,
GlDeprecatedButton,
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
GlFormCheckbox,
} from '@gitlab/ui';
-import eventHub from '~/clusters/event_hub';
import { mapValues } from 'lodash';
+import { __ } from '~/locale';
+import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
+import eventHub from '~/clusters/event_hub';
const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS;
@@ -17,8 +17,8 @@ export default {
components: {
GlAlert,
GlDeprecatedButton,
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
GlFormCheckbox,
},
props: {
@@ -203,15 +203,15 @@ export default {
<label for="fluentd-protocol">
<strong>{{ s__('ClusterIntegration|SIEM Protocol') }}</strong>
</label>
- <gl-dropdown :text="protocolName" class="w-100">
- <gl-dropdown-item
+ <gl-deprecated-dropdown :text="protocolName" class="w-100">
+ <gl-deprecated-dropdown-item
v-for="(value, index) in protocols"
:key="index"
@click="selectProtocol(value.toLowerCase())"
>
{{ value }}
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
</div>
<div class="form-group flex flex-wrap">
<gl-form-checkbox :checked="wafLogEnabled" @input="wafLogChanged">
diff --git a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
index 87c3225085f..5e8e1a76182 100644
--- a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
+++ b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
@@ -1,19 +1,19 @@
<script>
import { escape } from 'lodash';
-import { s__, __ } from '../../locale';
-import { APPLICATION_STATUS, INGRESS, LOGGING_MODE, BLOCKING_MODE } from '~/clusters/constants';
import {
GlAlert,
GlSprintf,
GlLink,
GlToggle,
GlDeprecatedButton,
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
GlIcon,
} from '@gitlab/ui';
-import eventHub from '~/clusters/event_hub';
import modSecurityLogo from 'images/cluster_app_logos/gitlab.png';
+import { s__, __ } from '../../locale';
+import { APPLICATION_STATUS, INGRESS, LOGGING_MODE, BLOCKING_MODE } from '~/clusters/constants';
+import eventHub from '~/clusters/event_hub';
const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS;
@@ -26,8 +26,8 @@ export default {
GlLink,
GlToggle,
GlDeprecatedButton,
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
GlIcon,
},
props: {
@@ -221,11 +221,15 @@ export default {
</strong>
</p>
</div>
- <gl-dropdown :text="modSecurityModeName" :disabled="saveButtonDisabled">
- <gl-dropdown-item v-for="(mode, key) in modes" :key="key" @click="selectMode(key)">
+ <gl-deprecated-dropdown :text="modSecurityModeName" :disabled="saveButtonDisabled">
+ <gl-deprecated-dropdown-item
+ v-for="(mode, key) in modes"
+ :key="key"
+ @click="selectMode(key)"
+ >
{{ mode.name }}
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
</div>
</div>
<div v-if="showButtons" class="mt-3">
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
index ac61cd8e242..1236d2a46c9 100644
--- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -1,8 +1,8 @@
<script>
import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownDivider,
+ GlDeprecatedDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
@@ -20,9 +20,9 @@ export default {
LoadingButton,
ClipboardButton,
GlLoadingIcon,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownDivider,
+ GlDeprecatedDropdownItem,
GlSearchBoxByType,
GlSprintf,
},
@@ -121,7 +121,7 @@ export default {
<strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
</label>
- <gl-dropdown
+ <gl-deprecated-dropdown
v-if="showDomainsDropdown"
:text="domainDropdownText"
toggle-class="dropdown-menu-toggle"
@@ -132,16 +132,16 @@ export default {
:placeholder="s__('ClusterIntegration|Search domains')"
class="m-2"
/>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="domain in filteredDomains"
:key="domain.id"
@click="selectDomain(domain)"
>
<span class="ml-1">{{ domain.domain }}</span>
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
<template v-if="searchQuery">
- <gl-dropdown-divider />
- <gl-dropdown-item key="custom-domain" @click="selectCustomDomain(searchQuery)">
+ <gl-deprecated-dropdown-divider />
+ <gl-deprecated-dropdown-item key="custom-domain" @click="selectCustomDomain(searchQuery)">
<span class="ml-1">
<gl-sprintf :message="s__('ClusterIntegration|Use %{query}')">
<template #query>
@@ -149,9 +149,9 @@ export default {
</template>
</gl-sprintf>
</span>
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
</template>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
<input
v-else
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index 45f2dd48961..3e3b102f0aa 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -1,7 +1,7 @@
<script>
import { escape } from 'lodash';
+import { GlModal, GlButton, GlDeprecatedButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import SplitButton from '~/vue_shared/components/split_button.vue';
-import { GlModal, GlButton, GlDeprecatedButton, GlFormInput } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
@@ -30,6 +30,7 @@ export default {
GlButton,
GlDeprecatedButton,
GlFormInput,
+ GlSprintf,
},
props: {
clusterPath: {
@@ -67,18 +68,6 @@ export default {
)
: s__('ClusterIntegration|You are about to remove your cluster integration.');
},
- warningToBeRemoved() {
- return s__(`ClusterIntegration|
- This will permanently delete the following resources:
- <ul>
- <li>All installed applications and related resources</li>
- <li>The <code>gitlab-managed-apps</code> namespace</li>
- <li>Any project namespaces</li>
- <li><code>clusterroles</code></li>
- <li><code>clusterrolebindings</code></li>
- </ul>
- `);
- },
confirmationTextLabel() {
return sprintf(
this.confirmCleanup
@@ -118,7 +107,7 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-flex gl-justify-content-end">
<split-button
v-if="canCleanupResources"
:action-items="$options.splitButtonActionItems"
@@ -144,9 +133,29 @@ export default {
>
<template>
<p>{{ warningMessage }}</p>
- <div v-if="confirmCleanup" v-html="warningToBeRemoved"></div>
+ <div v-if="confirmCleanup">
+ {{ s__('ClusterIntegration|This will permanently delete the following resources:') }}
+ <ul>
+ <li>
+ {{ s__('ClusterIntegration|All installed applications and related resources') }}
+ </li>
+ <li>
+ <gl-sprintf :message="s__('ClusterIntegration|The %{gitlabNamespace} namespace')">
+ <template #gitlabNamespace>
+ <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
+ <code>{{ 'gitlab-managed-apps' }}</code>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>{{ s__('ClusterIntegration|Any project namespaces') }}</li>
+ <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
+ <li><code>clusterroles</code></li>
+ <li><code>clusterrolebindings</code></li>
+ <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
+ </ul>
+ </div>
<strong v-html="confirmationTextLabel"></strong>
- <form ref="form" :action="clusterPath" method="post" class="append-bottom-20">
+ <form ref="form" :action="clusterPath" method="post" class="gl-mb-5">
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<input ref="cleanup" type="hidden" name="cleanup" value="true" />
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 60e179c54eb..e2227c61cee 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -25,6 +25,7 @@ export const APPLICATION_STATUS = {
UNINSTALL_ERRORED: 'uninstall_errored',
ERROR: 'errored',
PRE_INSTALLED: 'pre_installed',
+ UNINSTALLED: 'uninstalled',
};
/*
diff --git a/app/assets/javascripts/clusters/forms/components/integration_form.vue b/app/assets/javascripts/clusters/forms/components/integration_form.vue
new file mode 100644
index 00000000000..53e004b4fc0
--- /dev/null
+++ b/app/assets/javascripts/clusters/forms/components/integration_form.vue
@@ -0,0 +1,163 @@
+<script>
+import {
+ GlFormGroup,
+ GlFormInput,
+ GlToggle,
+ GlTooltipDirective,
+ GlSprintf,
+ GlLink,
+ GlButton,
+} from '@gitlab/ui';
+import { mapState } from 'vuex';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlToggle,
+ GlFormInput,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: {
+ autoDevopsHelpPath: {
+ type: String,
+ },
+ externalEndpointHelpPath: {
+ type: String,
+ },
+ },
+ data() {
+ return {
+ toggleEnabled: true,
+ envScope: '*',
+ baseDomainField: '',
+ externalIp: '',
+ };
+ },
+ computed: {
+ ...mapState([
+ 'enabled',
+ 'editable',
+ 'environmentScope',
+ 'baseDomain',
+ 'applicationIngressExternalIp',
+ ]),
+ canSubmit() {
+ return (
+ this.enabled !== this.toggleEnabled ||
+ this.environmentScope !== this.envScope ||
+ this.baseDomain !== this.baseDomainField
+ );
+ },
+ },
+ mounted() {
+ this.toggleEnabled = this.enabled;
+ this.envScope = this.environmentScope;
+ this.baseDomainField = this.baseDomain;
+ this.externalIp = this.applicationIngressExternalIp;
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex gl-flex-direction-column">
+ <gl-form-group>
+ <div class="gl-display-flex gl-align-items-center">
+ <h4 class="gl-pr-3 gl-m-0">{{ s__('ClusterIntegration|GitLab Integration') }}</h4>
+
+ <div class="js-cluster-enable-toggle-area">
+ <gl-toggle
+ id="toggleCluster"
+ v-model="toggleEnabled"
+ v-gl-tooltip:tooltipcontainer
+ name="cluster[enabled]"
+ class="gl-mb-0 js-project-feature-toggle"
+ data-qa-selector="integration_status_toggle"
+ aria-describedby="toggleCluster"
+ :disabled="!editable"
+ :title="
+ s__(
+ 'ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.',
+ )
+ "
+ />
+ </div>
+ </div>
+ </gl-form-group>
+
+ <gl-form-group
+ :label="s__('ClusterIntegration|Environment scope')"
+ label-size="sm"
+ label-for="cluster_environment_scope"
+ :description="
+ s__('ClusterIntegration|Choose which of your environments will use this cluster.')
+ "
+ >
+ <gl-form-input
+ id="cluster_environment_scope"
+ v-model="envScope"
+ name="cluster[environment_scope]"
+ class="col-md-6"
+ type="text"
+ />
+ </gl-form-group>
+
+ <gl-form-group
+ :label="s__('ClusterIntegration|Base domain')"
+ label-size="sm"
+ label-for="cluster_base_domain"
+ >
+ <gl-form-input
+ id="cluster_base_domain"
+ v-model="baseDomainField"
+ name="cluster[base_domain]"
+ data-qa-selector="base_domain_field"
+ class="col-md-6"
+ type="text"
+ />
+ <div class="form-text text-muted inline">
+ <gl-sprintf
+ :message="
+ s__(
+ 'ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{linkStart}Auto DevOps.%{linkEnd} The domain should have a wildcard DNS configured matching the domain. ',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="autoDevopsHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ <div v-if="applicationIngressExternalIp" class="js-ingress-domain-help-text inline">
+ {{ s__('ClusterIntegration|Alternatively, ') }}
+ <gl-sprintf :message="s__('ClusterIntegration|%{externalIp}.nip.io')">
+ <template #externalIp>{{ externalIp }}</template>
+ </gl-sprintf>
+ {{ s__('ClusterIntegration|can be used instead of a custom domain. ') }}
+ </div>
+ <gl-sprintf
+ class="inline"
+ :message="s__('ClusterIntegration|%{linkStart}More information%{linkEnd}')"
+ >
+ <template #link="{ content }">
+ <gl-link :href="externalEndpointHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </gl-form-group>
+ <div v-if="editable" class="form group gl-display-flex gl-justify-content-end">
+ <gl-button
+ category="primary"
+ variant="success"
+ type="submit"
+ :disabled="!canSubmit"
+ :aria-disabled="!canSubmit"
+ data-qa-selector="save_changes_button"
+ >{{ s__('ClusterIntegration|Save changes') }}</gl-button
+ >
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/forms/show/index.js b/app/assets/javascripts/clusters/forms/show/index.js
new file mode 100644
index 00000000000..47a3016c777
--- /dev/null
+++ b/app/assets/javascripts/clusters/forms/show/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import IntegrationForm from '../components/integration_form.vue';
+import { createStore } from '../stores';
+
+export default () => {
+ const entryPoint = document.querySelector('#js-cluster-integration-form');
+
+ if (!entryPoint) {
+ return;
+ }
+
+ const { autoDevopsHelpPath, externalEndpointHelpPath } = entryPoint.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: entryPoint,
+ store: createStore(entryPoint.dataset),
+ provide: {
+ autoDevopsHelpPath,
+ externalEndpointHelpPath,
+ },
+
+ render(createElement) {
+ return createElement(IntegrationForm, {});
+ },
+ });
+};
diff --git a/app/assets/javascripts/clusters/forms/stores/index.js b/app/assets/javascripts/clusters/forms/stores/index.js
new file mode 100644
index 00000000000..ae082c07f26
--- /dev/null
+++ b/app/assets/javascripts/clusters/forms/stores/index.js
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+
+Vue.use(Vuex);
+
+export const createStore = initialState =>
+ new Vuex.Store({
+ state: state(initialState),
+ });
+
+export default createStore;
diff --git a/app/assets/javascripts/clusters/forms/stores/state.js b/app/assets/javascripts/clusters/forms/stores/state.js
new file mode 100644
index 00000000000..2a96590b5e7
--- /dev/null
+++ b/app/assets/javascripts/clusters/forms/stores/state.js
@@ -0,0 +1,13 @@
+import { parseBoolean } from '../../../lib/utils/common_utils';
+
+export default (initialState = {}) => {
+ return {
+ enabled: parseBoolean(initialState.enabled),
+ editable: parseBoolean(initialState.editable),
+ environmentScope: initialState.environmentScope,
+ baseDomain: initialState.baseDomain,
+ applicationIngressExternalIp: initialState.applicationIngressExternalIp,
+ autoDevopsHelpPath: initialState.autoDevopsHelpPath,
+ externalEndpointHelpPath: initialState.externalEndpointHelpPath,
+ };
+};
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
index 6af9b10f12f..683b0e18534 100644
--- a/app/assets/javascripts/clusters/services/application_state_machine.js
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -14,6 +14,7 @@ const {
UNINSTALLING,
UNINSTALL_ERRORED,
PRE_INSTALLED,
+ UNINSTALLED,
} = APPLICATION_STATUS;
const applicationStateMachine = {
@@ -67,6 +68,9 @@ const applicationStateMachine = {
[PRE_INSTALLED]: {
target: PRE_INSTALLED,
},
+ [UNINSTALLED]: {
+ target: UNINSTALLED,
+ },
},
},
[NOT_INSTALLABLE]: {
@@ -87,9 +91,17 @@ const applicationStateMachine = {
[NOT_INSTALLABLE]: {
target: NOT_INSTALLABLE,
},
- // This is possible in artificial environments for E2E testing
[INSTALLED]: {
target: INSTALLED,
+ effects: {
+ installFailed: false,
+ },
+ },
+ [UNINSTALLED]: {
+ target: UNINSTALLED,
+ effects: {
+ installFailed: false,
+ },
},
},
},
@@ -125,6 +137,15 @@ const applicationStateMachine = {
uninstallSuccessful: false,
},
},
+ [UNINSTALLED]: {
+ target: UNINSTALLED,
+ },
+ [ERROR]: {
+ target: INSTALLABLE,
+ effects: {
+ installFailed: true,
+ },
+ },
},
},
[PRE_INSTALLED]: {
@@ -180,6 +201,19 @@ const applicationStateMachine = {
},
},
},
+ [UNINSTALLED]: {
+ on: {
+ [INSTALLED]: {
+ target: INSTALLED,
+ },
+ [ERROR]: {
+ target: INSTALLABLE,
+ effects: {
+ installFailed: true,
+ },
+ },
+ },
+ },
};
/**
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 9d354e66661..53868b7c02d 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -23,6 +23,7 @@ const applicationInitialState = {
status: null,
statusReason: null,
requestReason: null,
+ installable: true,
installed: false,
installFailed: false,
uninstallable: false,
@@ -114,6 +115,11 @@ export default class ClusterStore {
ciliumLogEnabled: null,
isEditingSettings: false,
},
+ cilium: {
+ ...applicationInitialState,
+ title: s__('ClusterIntegration|GitLab Container Network Policies'),
+ installable: false,
+ },
},
environments: [],
fetchingEnvironments: false,
@@ -129,6 +135,7 @@ export default class ClusterStore {
clustersHelpPath,
deployBoardsHelpPath,
cloudRunHelpPath,
+ ciliumHelpPath,
) {
this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath;
@@ -138,6 +145,7 @@ export default class ClusterStore {
this.state.clustersHelpPath = clustersHelpPath;
this.state.deployBoardsHelpPath = deployBoardsHelpPath;
this.state.cloudRunHelpPath = cloudRunHelpPath;
+ this.state.ciliumHelpPath = ciliumHelpPath;
}
setManagePrometheusPath(managePrometheusPath) {
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 7e9b720d269..09d7c0329a9 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -231,7 +231,7 @@ export default {
<gl-skeleton-loading v-else-if="loadingNodes" :lines="1" :class="contentAlignClasses" />
- <small v-else class="gl-font-sm gl-font-style-italic gl-text-gray-400">{{
+ <small v-else class="gl-font-sm gl-font-style-italic gl-text-gray-200">{{
__('Unknown')
}}</small>
</template>
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index dddcfb3d975..ff711877621 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -1,10 +1,10 @@
+import * as Sentry from '@sentry/browser';
import Poll from '~/lib/utils/poll';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import { MAX_REQUESTS } from '../constants';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
-import * as Sentry from '@sentry/browser';
import * as types from './mutation_types';
const allNodesPresent = (clusters, retryCount) => {
@@ -76,6 +76,3 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
export const setPage = ({ commit }, page) => {
commit(types.SET_PAGE, page);
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 23a842fab4c..1526d994770 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import PipelinesService from '~/pipelines/services/pipelines_service';
import PipelineStore from '~/pipelines/stores/pipelines_store';
@@ -12,8 +12,10 @@ import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
export default {
components: {
TablePagination,
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
+ GlModal,
+ GlLink,
},
mixins: [pipelinesMixin, CIPaginationMixin],
props: {
@@ -38,11 +40,21 @@ export default {
required: false,
default: 'child',
},
- canRunPipeline: {
+ canCreatePipelineInTargetProject: {
type: Boolean,
required: false,
default: false,
},
+ sourceProjectFullPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ targetProjectFullPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
projectId: {
type: String,
required: false,
@@ -63,6 +75,7 @@ export default {
state: store.state,
page: getParameterByName('page') || '1',
requestData: {},
+ modalId: 'create-pipeline-for-fork-merge-request-modal',
};
},
@@ -75,13 +88,28 @@ export default {
},
/**
* The Run Pipeline button can only be rendered when:
- * - In MR view - we use `canRunPipeline` for that purpose
+ * - In MR view - we use `canCreatePipelineInTargetProject` for that purpose
* - If the latest pipeline has the `detached_merge_request_pipeline` flag
*
* @returns {Boolean}
*/
canRenderPipelineButton() {
- return this.canRunPipeline && this.latestPipelineDetachedFlag;
+ return this.latestPipelineDetachedFlag;
+ },
+ isForkMergeRequest() {
+ return this.sourceProjectFullPath !== this.targetProjectFullPath;
+ },
+ isLatestPipelineCreatedInTargetProject() {
+ const latest = this.state.pipelines[0];
+
+ return latest?.project?.full_path === `/${this.targetProjectFullPath}`;
+ },
+ shouldShowSecurityWarning() {
+ return (
+ this.canCreatePipelineInTargetProject &&
+ this.isForkMergeRequest &&
+ !this.isLatestPipelineCreatedInTargetProject
+ );
},
/**
* Checks if either `detached_merge_request_pipeline` or
@@ -148,6 +176,13 @@ export default {
mergeRequestId: this.mergeRequestId,
});
},
+ tryRunPipeline() {
+ if (!this.shouldShowSecurityWarning) {
+ this.onClickRunPipeline();
+ } else {
+ this.$refs.modal.show();
+ }
+ },
},
};
</script>
@@ -171,16 +206,53 @@ export default {
<div v-else-if="shouldRenderTable" class="table-holder">
<div v-if="canRenderPipelineButton" class="nav justify-content-end">
- <gl-deprecated-button
- v-if="canRenderPipelineButton"
+ <gl-button
variant="success"
- class="js-run-mr-pipeline prepend-top-10 btn-wide-on-xs"
+ class="js-run-mr-pipeline gl-mt-3 btn-wide-on-xs"
:disabled="state.isRunningMergeRequestPipeline"
- @click="onClickRunPipeline"
+ @click="tryRunPipeline"
>
<gl-loading-icon v-if="state.isRunningMergeRequestPipeline" inline />
{{ s__('Pipelines|Run Pipeline') }}
- </gl-deprecated-button>
+ </gl-button>
+
+ <gl-modal
+ :id="modalId"
+ ref="modal"
+ :modal-id="modalId"
+ :title="s__('Pipelines|Are you sure you want to run this pipeline?')"
+ :ok-title="s__('Pipelines|Run Pipeline')"
+ ok-variant="danger"
+ @ok="onClickRunPipeline"
+ >
+ <p>
+ {{
+ s__(
+ 'Pipelines|This pipeline will run code originating from a forked project merge request. This means that the code can potentially have security considerations like exposing CI variables.',
+ )
+ }}
+ </p>
+ <p>
+ {{
+ s__(
+ "Pipelines|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource.",
+ )
+ }}
+ </p>
+ <p>
+ {{
+ s__(
+ 'Pipelines|If you are unsure, please ask a project maintainer to review it for you.',
+ )
+ }}
+ </p>
+ <gl-link
+ href="/help/ci/merge_request_pipelines/index.html#create-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
+ target="_blank"
+ >
+ {{ s__('Pipelines|More Information') }}
+ </gl-link>
+ </gl-modal>
</div>
<pipelines-table-component
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 4539b9a39ef..34322755fe9 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import { capitalizeFirstCharacter } from './lib/utils/text_utility';
export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
index 92a5423d5ea..b8163ecfab2 100644
--- a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
@@ -1,12 +1,12 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
Icon,
},
props: {
@@ -38,7 +38,7 @@ export default {
</script>
<template>
- <gl-dropdown toggle-class="d-flex align-items-center w-100" class="w-100">
+ <gl-deprecated-dropdown toggle-class="d-flex align-items-center w-100" class="w-100">
<template #button-content>
<span class="str-truncated-100 mr-2">
<icon name="lock" />
@@ -46,13 +46,17 @@ export default {
</span>
<icon name="chevron-down" class="ml-auto" />
</template>
- <gl-dropdown-item v-for="project in projects" :key="project.id" @click="selectProject(project)">
+ <gl-deprecated-dropdown-item
+ v-for="project in projects"
+ :key="project.id"
+ @click="selectProject(project)"
+ >
<icon
name="mobile-issue-close"
:class="{ icon: project.id !== selectedProject.id }"
class="js-active-project-check"
/>
<span class="ml-1">{{ project.name }}</span>
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
</template>
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 f2853564f94..88459d5962e 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { __ } from '../../locale';
-import createFlash from '../../flash';
+import { deprecatedCreateFlash as createFlash } from '../../flash';
import Api from '../../api';
import state from '../state';
import Dropdown from './dropdown.vue';
diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js
index 4138ff24f1d..393af932fb0 100644
--- a/app/assets/javascripts/contributors/stores/actions.js
+++ b/app/assets/javascripts/contributors/stores/actions.js
@@ -1,8 +1,9 @@
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import service from '../services/contributors_service';
import * as types from './mutation_types';
+// eslint-disable-next-line import/prefer-default-export
export const fetchChartData = ({ commit }, endpoint) => {
commit(types.SET_LOADING_STATE, true);
@@ -15,6 +16,3 @@ export const fetchChartData = ({ commit }, endpoint) => {
})
.catch(() => flash(__('An error occurred while loading chart data')));
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/contributors/stores/getters.js b/app/assets/javascripts/contributors/stores/getters.js
index 9b0def9b3ca..9022179d6c7 100644
--- a/app/assets/javascripts/contributors/stores/getters.js
+++ b/app/assets/javascripts/contributors/stores/getters.js
@@ -28,6 +28,3 @@ export const parsedData = state => {
byAuthorEmail,
};
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
index e96e6d6e4f8..caf2729a4c7 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/store/actions.js
@@ -1,7 +1,7 @@
import * as types from './mutation_types';
import { setAWSConfig } from '../services/aws_services_facade';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const getErrorMessage = data => {
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
index b0bec10f64d..979628d683d 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
@@ -1,12 +1,16 @@
<script>
-import { escape } from 'lodash';
import { mapState, mapGetters, mapActions } from 'vuex';
-import { s__, sprintf } from '~/locale';
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
import gkeDropdownMixin from './gke_dropdown_mixin';
export default {
name: 'GkeProjectIdDropdown',
+ components: {
+ GlSprintf,
+ GlLink,
+ },
mixins: [gkeDropdownMixin],
props: {
docsUrl: {
@@ -46,31 +50,23 @@ export default {
return s__('ClusterIntegration|Select project');
},
helpText() {
- let message;
if (this.hasErrors) {
return this.errorMessage;
}
if (!this.items) {
- message =
- 'ClusterIntegration|We were unable to fetch any projects. Ensure that you have a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';
+ return s__(
+ 'ClusterIntegration|We were unable to fetch any projects. Ensure that you have a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
+ );
}
- message =
- this.items && this.items.length
- ? 'ClusterIntegration|To use a new project, first create one on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.'
- : 'ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';
-
- return sprintf(
- s__(message),
- {
- docsLinkEnd: '&nbsp;<i class="fa fa-external-link" aria-hidden="true"></i></a>',
- docsLinkStart: `<a href="${escape(
- this.docsUrl,
- )}" target="_blank" rel="noopener noreferrer">`,
- },
- false,
- );
+ return this.items.length
+ ? s__(
+ 'ClusterIntegration|To use a new project, first create one on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
+ )
+ : s__(
+ 'ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.',
+ );
},
errorMessage() {
if (!this.projectHasBillingEnabled) {
@@ -80,21 +76,13 @@ export default {
);
}
- return sprintf(
- s__(
- 'This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target="_blank" rel="noopener noreferrer">enable billing <i class="fa fa-external-link" aria-hidden="true"></i></a> and try again.',
- ),
- {
- linkToBilling:
- 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral',
- },
- false,
+ return s__(
+ 'ClusterIntegration|This project does not have billing enabled. To create a cluster, %{linkToBillingStart}enable billing%{linkToBillingEnd} and try again.',
);
}
- return sprintf(
- s__('ClusterIntegration|An error occurred while trying to fetch your projects: %{error}'),
- { error: this.gapiError },
+ return s__(
+ 'ClusterIntegration|An error occurred while trying to fetch your projects: %{error}',
);
},
},
@@ -182,7 +170,28 @@ export default {
'text-muted': !hasErrors,
}"
class="form-text"
- v-html="helpText"
- ></span>
+ >
+ <gl-sprintf :message="helpText">
+ <template #linkToBilling="{ content }">
+ <gl-link
+ :href="
+ 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral'
+ "
+ target="_blank"
+ >{{ content }} <i class="fa fa-external-link" aria-hidden="true"></i
+ ></gl-link>
+ </template>
+
+ <template #docsLink="{ content }">
+ <gl-link :href="docsUrl" target="_blank"
+ >{{ content }} <i class="fa fa-external-link" aria-hidden="true"></i
+ ></gl-link>
+ </template>
+
+ <template #error>
+ {{ gapiError }}
+ </template>
+ </gl-sprintf>
+ </span>
</div>
</template>
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/index.js b/app/assets/javascripts/create_cluster/gke_cluster/index.js
index 5a64eb09cad..b9316353072 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/index.js
+++ b/app/assets/javascripts/create_cluster/gke_cluster/index.js
@@ -1,6 +1,6 @@
/* global gapi */
import Vue from 'vue';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
import GkeZoneDropdown from './components/gke_zone_dropdown.vue';
import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue';
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js b/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
index f05ad7773a2..f0c41d1d230 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
+++ b/app/assets/javascripts/create_cluster/gke_cluster/store/actions.js
@@ -90,6 +90,3 @@ export const fetchMachineTypes = ({ commit, state }) =>
mutation: types.SET_MACHINE_TYPES,
payloadKey: 'items',
});
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index 801566d2f2f..010a6b073f9 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
import { debounce } from 'lodash';
import axios from './lib/utils/axios_utils';
-import Flash from './flash';
+import { deprecatedCreateFlash as Flash } from './flash';
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
import { __, sprintf } from './locale';
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
index f60be52d6ca..9c28801306c 100644
--- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import csrf from '~/lib/utils/csrf';
import CustomMetricsFormFields from './custom_metrics_form_fields.vue';
@@ -10,7 +10,7 @@ export default {
components: {
CustomMetricsFormFields,
DeleteCustomMetricModal,
- GlDeprecatedButton,
+ GlButton,
},
props: {
customMetricsPath: {
@@ -76,15 +76,10 @@ export default {
@formValidation="formValidation"
/>
<div class="form-actions">
- <gl-deprecated-button variant="success" :disabled="!formIsValid" @click="submit">
+ <gl-button variant="success" category="primary" :disabled="!formIsValid" @click="submit">
{{ saveButtonText }}
- </gl-deprecated-button>
- <gl-deprecated-button
- variant="secondary"
- class="float-right"
- :href="editProjectServicePath"
- >{{ __('Cancel') }}</gl-deprecated-button
- >
+ </gl-button>
+ <gl-button class="float-right" :href="editProjectServicePath">{{ __('Cancel') }}</gl-button>
<delete-custom-metric-modal
v-if="metricPersisted"
:delete-metric-url="customMetricsPath"
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
index d61e6995551..dc13f409462 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
import limitWarning from './limit_warning_component.vue';
import totalTime from './total_time_component.vue';
@@ -10,6 +11,7 @@ export default {
totalTime,
limitWarning,
icon,
+ GlIcon,
},
props: {
items: {
@@ -52,7 +54,8 @@ export default {
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
- <i class="fa fa-ban" aria-hidden="true"> </i> {{ mergeRequest.state.toUpperCase() }}
+ <gl-icon name="cancel" class="gl-vertical-align-text-bottom" />
+ {{ __('CLOSED') }}
</span>
</template>
<template v-else>
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index f609ca5f22d..f6bad5dce41 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -2,8 +2,7 @@ import $ from 'jquery';
import Vue from 'vue';
import Cookies from 'js-cookie';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
-import filterMixins from 'ee_else_ce/analytics/cycle_analytics/mixins/filter_mixins';
-import Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import { __ } from '~/locale';
import Translate from '../vue_shared/translate';
import banner from './components/banner.vue';
@@ -45,7 +44,6 @@ export default () => {
import('ee_component/analytics/shared/components/date_range_dropdown.vue'),
'stage-nav-item': stageNavItem,
},
- mixins: [filterMixins],
data() {
return {
store: CycleAnalyticsStore,
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
new file mode 100644
index 00000000000..afc1c2cda8e
--- /dev/null
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
@@ -0,0 +1,149 @@
+<script>
+import { GlFormGroup, GlFormInput, GlModal, GlSprintf, GlLink } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { isValidCron } from 'cron-validator';
+import { mapComputed } from '~/vuex_shared/bindings';
+import { __ } from '~/locale';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ GlModal,
+ GlSprintf,
+ GlLink,
+ TimezoneDropdown,
+ },
+ modalOptions: {
+ ref: 'modal',
+ modalId: 'deploy-freeze-modal',
+ title: __('Add deploy freeze'),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ static: true,
+ lazy: true,
+ },
+ translations: {
+ cronPlaceholder: __('* * * * *'),
+ cronSyntaxInstructions: __(
+ 'Define a custom deploy freeze pattern with %{cronSyntaxStart}cron syntax%{cronSyntaxEnd}',
+ ),
+ },
+ computed: {
+ ...mapState([
+ 'projectId',
+ 'selectedTimezone',
+ 'timezoneData',
+ 'freezeStartCron',
+ 'freezeEndCron',
+ ]),
+ ...mapComputed([
+ { key: 'freezeStartCron', updateFn: 'setFreezeStartCron' },
+ { key: 'freezeEndCron', updateFn: 'setFreezeEndCron' },
+ ]),
+ addDeployFreezeButton() {
+ return {
+ text: __('Add deploy freeze'),
+ attributes: [
+ { variant: 'success' },
+ {
+ disabled:
+ !isValidCron(this.freezeStartCron) ||
+ !isValidCron(this.freezeEndCron) ||
+ !this.selectedTimezone,
+ },
+ ],
+ };
+ },
+ invalidFreezeStartCron() {
+ return this.invalidCronMessage(this.freezeStartCronState);
+ },
+ freezeStartCronState() {
+ return Boolean(!this.freezeStartCron || isValidCron(this.freezeStartCron));
+ },
+ invalidFreezeEndCron() {
+ return this.invalidCronMessage(this.freezeEndCronState);
+ },
+ freezeEndCronState() {
+ return Boolean(!this.freezeEndCron || isValidCron(this.freezeEndCron));
+ },
+ timezone: {
+ get() {
+ return this.selectedTimezone;
+ },
+ set(selectedTimezone) {
+ this.setSelectedTimezone(selectedTimezone);
+ },
+ },
+ },
+ methods: {
+ ...mapActions(['addFreezePeriod', 'setSelectedTimezone', 'resetModal']),
+ resetModalHandler() {
+ this.resetModal();
+ },
+ invalidCronMessage(validCronState) {
+ if (!validCronState) {
+ return __('This Cron pattern is invalid');
+ }
+ return '';
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ v-bind="$options.modalOptions"
+ :action-primary="addDeployFreezeButton"
+ @primary="addFreezePeriod"
+ @canceled="resetModalHandler"
+ >
+ <p>
+ <gl-sprintf :message="$options.translations.cronSyntaxInstructions">
+ <template #cronSyntax="{ content }">
+ <gl-link href="https://crontab.guru/" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <gl-form-group
+ :label="__('Freeze start')"
+ label-for="deploy-freeze-start"
+ :invalid-feedback="invalidFreezeStartCron"
+ :state="freezeStartCronState"
+ >
+ <gl-form-input
+ id="deploy-freeze-start"
+ v-model="freezeStartCron"
+ class="gl-font-monospace!"
+ data-qa-selector="deploy_freeze_start_field"
+ :placeholder="this.$options.translations.cronPlaceholder"
+ :state="freezeStartCronState"
+ trim
+ />
+ </gl-form-group>
+
+ <gl-form-group
+ :label="__('Freeze end')"
+ label-for="deploy-freeze-end"
+ :invalid-feedback="invalidFreezeEndCron"
+ :state="freezeEndCronState"
+ >
+ <gl-form-input
+ id="deploy-freeze-end"
+ v-model="freezeEndCron"
+ class="gl-font-monospace!"
+ data-qa-selector="deploy_freeze_end_field"
+ :placeholder="this.$options.translations.cronPlaceholder"
+ :state="freezeEndCronState"
+ trim
+ />
+ </gl-form-group>
+
+ <gl-form-group :label="__('Cron time zone')" label-for="cron-time-zone-dropdown">
+ <timezone-dropdown v-model="timezone" :timezone-data="timezoneData" />
+ </gl-form-group>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue
new file mode 100644
index 00000000000..fc2ed10f3ca
--- /dev/null
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue
@@ -0,0 +1,18 @@
+<script>
+import DeployFreezeTable from './deploy_freeze_table.vue';
+import DeployFreezeModal from './deploy_freeze_modal.vue';
+
+export default {
+ components: {
+ DeployFreezeTable,
+ DeployFreezeModal,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <deploy-freeze-table />
+ <deploy-freeze-modal />
+ </div>
+</template>
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue
new file mode 100644
index 00000000000..159f5ddd755
--- /dev/null
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlTable, GlButton, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { s__, __ } from '~/locale';
+
+export default {
+ fields: [
+ {
+ key: 'freezeStart',
+ label: s__('DeployFreeze|Freeze start'),
+ },
+ {
+ key: 'freezeEnd',
+ label: s__('DeployFreeze|Freeze end'),
+ },
+ {
+ key: 'cronTimezone',
+ label: s__('DeployFreeze|Time zone'),
+ },
+ ],
+ translations: {
+ addDeployFreeze: __('Add deploy freeze'),
+ },
+ components: {
+ GlTable,
+ GlButton,
+ GlSprintf,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ computed: {
+ ...mapState(['freezePeriods']),
+ tableIsNotEmpty() {
+ return this.freezePeriods?.length > 0;
+ },
+ },
+ mounted() {
+ this.fetchFreezePeriods();
+ },
+ methods: {
+ ...mapActions(['fetchFreezePeriods']),
+ },
+};
+</script>
+
+<template>
+ <div class="deploy-freeze-table">
+ <gl-table
+ data-testid="deploy-freeze-table"
+ :items="freezePeriods"
+ :fields="$options.fields"
+ show-empty
+ stacked="lg"
+ >
+ <template #empty>
+ <p data-testid="empty-freeze-periods" class="gl-text-center text-plain">
+ <gl-sprintf
+ :message="
+ s__(
+ 'DeployFreeze|No deploy freezes exist for this project. To add one, click %{strongStart}Add deploy freeze%{strongEnd}',
+ )
+ "
+ >
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </gl-table>
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-button
+ v-gl-modal.deploy-freeze-modal
+ data-testid="add-deploy-freeze"
+ category="primary"
+ variant="success"
+ >
+ {{ $options.translations.addDeployFreeze }}
+ </gl-button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/deploy_freeze/index.js b/app/assets/javascripts/deploy_freeze/index.js
new file mode 100644
index 00000000000..fd3f52b6da1
--- /dev/null
+++ b/app/assets/javascripts/deploy_freeze/index.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import DeployFreezeSettings from './components/deploy_freeze_settings.vue';
+import createStore from './store';
+
+export default () => {
+ const el = document.getElementById('js-deploy-freeze-table');
+
+ const { projectId, timezoneData } = el.dataset;
+
+ const store = createStore({
+ projectId,
+ timezoneData: JSON.parse(timezoneData),
+ });
+
+ return new Vue({
+ el,
+ store,
+ render(createElement) {
+ return createElement(DeployFreezeSettings);
+ },
+ });
+};
diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js
new file mode 100644
index 00000000000..2fbbba5a128
--- /dev/null
+++ b/app/assets/javascripts/deploy_freeze/store/actions.js
@@ -0,0 +1,63 @@
+import * as types from './mutation_types';
+import Api from '~/api';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { __ } from '~/locale';
+
+export const requestAddFreezePeriod = ({ commit }) => {
+ commit(types.REQUEST_ADD_FREEZE_PERIOD);
+};
+
+export const receiveAddFreezePeriodSuccess = ({ commit }) => {
+ commit(types.RECEIVE_ADD_FREEZE_PERIOD_SUCCESS);
+};
+
+export const receiveAddFreezePeriodError = ({ commit }, error) => {
+ commit(types.RECEIVE_ADD_FREEZE_PERIOD_ERROR, error);
+};
+
+export const addFreezePeriod = ({ state, dispatch, commit }) => {
+ dispatch('requestAddFreezePeriod');
+
+ return Api.createFreezePeriod(state.projectId, {
+ freeze_start: state.freezeStartCron,
+ freeze_end: state.freezeEndCron,
+ cron_timezone: state.selectedTimezoneIdentifier,
+ })
+ .then(() => {
+ dispatch('receiveAddFreezePeriodSuccess');
+ commit(types.RESET_MODAL);
+ dispatch('fetchFreezePeriods');
+ })
+ .catch(error => {
+ createFlash(__('Error: Unable to create deploy freeze'));
+ dispatch('receiveAddFreezePeriodError', error);
+ });
+};
+
+export const fetchFreezePeriods = ({ commit, state }) => {
+ commit(types.REQUEST_FREEZE_PERIODS);
+
+ return Api.freezePeriods(state.projectId)
+ .then(({ data }) => {
+ commit(types.RECEIVE_FREEZE_PERIODS_SUCCESS, data);
+ })
+ .catch(() => {
+ createFlash(__('There was an error fetching the deploy freezes.'));
+ });
+};
+
+export const setSelectedTimezone = ({ commit }, timezone) => {
+ commit(types.SET_SELECTED_TIMEZONE, timezone);
+};
+
+export const setFreezeStartCron = ({ commit }, { freezeStartCron }) => {
+ commit(types.SET_FREEZE_START_CRON, freezeStartCron);
+};
+
+export const setFreezeEndCron = ({ commit }, { freezeEndCron }) => {
+ commit(types.SET_FREEZE_END_CRON, freezeEndCron);
+};
+
+export const resetModal = ({ commit }) => {
+ commit(types.RESET_MODAL);
+};
diff --git a/app/assets/javascripts/deploy_freeze/store/index.js b/app/assets/javascripts/deploy_freeze/store/index.js
new file mode 100644
index 00000000000..ca7ea8c783c
--- /dev/null
+++ b/app/assets/javascripts/deploy_freeze/store/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import mutations from './mutations';
+import createState from './state';
+
+Vue.use(Vuex);
+
+export default initialState =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ state: createState(initialState),
+ });
diff --git a/app/assets/javascripts/deploy_freeze/store/mutation_types.js b/app/assets/javascripts/deploy_freeze/store/mutation_types.js
new file mode 100644
index 00000000000..47a4874a5cf
--- /dev/null
+++ b/app/assets/javascripts/deploy_freeze/store/mutation_types.js
@@ -0,0 +1,12 @@
+export const REQUEST_FREEZE_PERIODS = 'REQUEST_FREEZE_PERIODS';
+export const RECEIVE_FREEZE_PERIODS_SUCCESS = 'RECEIVE_FREEZE_PERIODS_SUCCESS';
+
+export const REQUEST_ADD_FREEZE_PERIOD = 'REQUEST_ADD_FREEZE_PERIOD';
+export const RECEIVE_ADD_FREEZE_PERIOD_SUCCESS = 'RECEIVE_ADD_FREEZE_PERIOD_SUCCESS';
+export const RECEIVE_ADD_FREEZE_PERIOD_ERROR = 'RECEIVE_ADD_FREEZE_PERIOD_ERROR';
+
+export const SET_SELECTED_TIMEZONE = 'SET_SELECTED_TIMEZONE';
+export const SET_FREEZE_START_CRON = 'SET_FREEZE_START_CRON';
+export const SET_FREEZE_END_CRON = 'SET_FREEZE_END_CRON';
+
+export const RESET_MODAL = 'RESET_MODAL';
diff --git a/app/assets/javascripts/deploy_freeze/store/mutations.js b/app/assets/javascripts/deploy_freeze/store/mutations.js
new file mode 100644
index 00000000000..89ce1dc5428
--- /dev/null
+++ b/app/assets/javascripts/deploy_freeze/store/mutations.js
@@ -0,0 +1,54 @@
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import * as types from './mutation_types';
+
+const formatTimezoneName = (freezePeriod, timezoneList) =>
+ convertObjectPropsToCamelCase({
+ ...freezePeriod,
+ cron_timezone: timezoneList.find(tz => tz.identifier === freezePeriod.cron_timezone)?.name,
+ });
+
+export default {
+ [types.REQUEST_FREEZE_PERIODS](state) {
+ state.isLoading = true;
+ },
+
+ [types.RECEIVE_FREEZE_PERIODS_SUCCESS](state, freezePeriods) {
+ state.isLoading = false;
+ state.freezePeriods = freezePeriods.map(freezePeriod =>
+ formatTimezoneName(freezePeriod, state.timezoneData),
+ );
+ },
+
+ [types.REQUEST_ADD_FREEZE_PERIOD](state) {
+ state.isLoading = true;
+ },
+
+ [types.RECEIVE_ADD_FREEZE_PERIOD_SUCCESS](state) {
+ state.isLoading = false;
+ },
+
+ [types.RECEIVE_ADD_FREEZE_PERIOD_ERROR](state, error) {
+ state.isLoading = false;
+ state.error = error;
+ },
+
+ [types.SET_SELECTED_TIMEZONE](state, timezone) {
+ state.selectedTimezone = timezone.formattedTimezone;
+ state.selectedTimezoneIdentifier = timezone.identifier;
+ },
+
+ [types.SET_FREEZE_START_CRON](state, freezeStartCron) {
+ state.freezeStartCron = freezeStartCron;
+ },
+
+ [types.SET_FREEZE_END_CRON](state, freezeEndCron) {
+ state.freezeEndCron = freezeEndCron;
+ },
+
+ [types.RESET_MODAL](state) {
+ state.freezeStartCron = '';
+ state.freezeEndCron = '';
+ state.selectedTimezone = '';
+ state.selectedTimezoneIdentifier = '';
+ },
+};
diff --git a/app/assets/javascripts/deploy_freeze/store/state.js b/app/assets/javascripts/deploy_freeze/store/state.js
new file mode 100644
index 00000000000..4cc38c097b6
--- /dev/null
+++ b/app/assets/javascripts/deploy_freeze/store/state.js
@@ -0,0 +1,17 @@
+export default ({
+ projectId,
+ freezePeriods = [],
+ timezoneData = [],
+ selectedTimezone = '',
+ selectedTimezoneIdentifier = '',
+ freezeStartCron = '',
+ freezeEndCron = '',
+}) => ({
+ projectId,
+ freezePeriods,
+ timezoneData,
+ selectedTimezone,
+ selectedTimezoneIdentifier,
+ freezeStartCron,
+ freezeEndCron,
+});
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 1b8668b533e..a03a7114b40 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue
index 1fd902c9ed7..37686dd5a46 100644
--- a/app/assets/javascripts/design_management/components/delete_button.vue
+++ b/app/assets/javascripts/design_management/components/delete_button.vue
@@ -1,11 +1,12 @@
<script>
-import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash';
+import { s__, __ } from '~/locale';
export default {
name: 'DeleteButton',
components: {
- GlDeprecatedButton,
+ GlButton,
GlModal,
},
directives: {
@@ -25,40 +26,78 @@ export default {
buttonVariant: {
type: String,
required: false,
- default: '',
+ default: 'info',
+ },
+ buttonCategory: {
+ type: String,
+ required: false,
+ default: 'primary',
+ },
+ buttonIcon: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ buttonSize: {
+ type: String,
+ required: false,
+ default: 'medium',
},
hasSelectedDesigns: {
type: Boolean,
required: false,
default: true,
},
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
modalId: uniqueId('design-deletion-confirmation-'),
};
},
+ modal: {
+ title: s__('DesignManagement|Are you sure you want to archive the selected designs?'),
+ actionPrimary: {
+ text: s__('DesignManagement|Archive designs'),
+ attributes: { variant: 'warning' },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ },
};
</script>
<template>
- <div>
+ <div class="gl-display-flex gl-align-items-center gl-h-full">
<gl-modal
:modal-id="modalId"
- :title="s__('DesignManagement|Delete designs confirmation')"
- :ok-title="s__('DesignManagement|Delete')"
- ok-variant="danger"
+ :title="$options.modal.title"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
@ok="$emit('deleteSelectedDesigns')"
>
- <p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p>
+ <p>
+ {{
+ s__(
+ 'DesignManagement|Archived designs will still be available in previous versions of the design collection.',
+ )
+ }}
+ </p>
</gl-modal>
- <gl-deprecated-button
+ <gl-button
v-gl-modal-directive="modalId"
:variant="buttonVariant"
- :disabled="isDeleting || !hasSelectedDesigns"
+ :category="buttonCategory"
+ :size="buttonSize"
:class="buttonClass"
- >
- <slot></slot>
- </gl-deprecated-button>
+ :loading="loading"
+ :icon="buttonIcon"
+ :disabled="isDeleting || !hasSelectedDesigns"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/design_management/components/design_destroyer.vue b/app/assets/javascripts/design_management/components/design_destroyer.vue
index 62460ca551c..7ae569216f0 100644
--- a/app/assets/javascripts/design_management/components/design_destroyer.vue
+++ b/app/assets/javascripts/design_management/components/design_destroyer.vue
@@ -13,13 +13,14 @@ export default {
type: Array,
required: true,
},
+ },
+ inject: {
projectPath: {
- type: String,
- required: true,
+ default: '',
},
iid: {
- type: String,
- required: true,
+ from: 'issueIid',
+ defaut: '',
},
},
computed: {
diff --git a/app/assets/javascripts/design_management/components/design_note_pin.vue b/app/assets/javascripts/design_management/components/design_note_pin.vue
index 0811397fbad..2b5e62c2870 100644
--- a/app/assets/javascripts/design_management/components/design_note_pin.vue
+++ b/app/assets/javascripts/design_management/components/design_note_pin.vue
@@ -1,11 +1,11 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'DesignNotePin',
components: {
- Icon,
+ GlIcon,
},
props: {
position: {
@@ -47,13 +47,13 @@ export default {
'btn-transparent comment-indicator': isNewNote,
'js-image-badge badge badge-pill': !isNewNote,
}"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center"
+ class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0"
type="button"
@mousedown="$emit('mousedown', $event)"
@mouseup="$emit('mouseup', $event)"
@click="$emit('click', $event)"
>
- <icon v-if="isNewNote" name="image-comment-dark" />
+ <gl-icon v-if="isNewNote" name="image-comment-dark" :size="24" />
<template v-else>
{{ label }}
</template>
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 4aaf43e3a5b..6a20517eed7 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
@@ -230,10 +230,10 @@ export default {
</button>
</template>
<template v-if="discussion.resolved" #resolvedStatus>
- <p class="gl-text-gray-700 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message">
+ <p class="gl-text-gray-500 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message">
{{ __('Resolved by') }}
<gl-link
- class="gl-text-gray-700 gl-text-decoration-none gl-font-sm link-inherit-color"
+ class="gl-text-gray-500 gl-text-decoration-none gl-font-sm link-inherit-color"
:href="discussion.resolvedBy.webUrl"
target="_blank"
>{{ discussion.resolvedBy.name }}</gl-link
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index b1f3a43a66d..172e61920ef 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -60,7 +60,7 @@ export default {
},
mounted() {
if (this.isNoteLinked) {
- this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
+ this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
}
},
methods: {
@@ -80,7 +80,7 @@ export default {
</script>
<template>
- <timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form">
+ <timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form">
<user-avatar-link
:link-href="author.webUrl"
:img-src="author.avatarUrl"
diff --git a/app/assets/javascripts/design_management/components/design_notes/toggle_replies_widget.vue b/app/assets/javascripts/design_management/components/design_notes/toggle_replies_widget.vue
index 46c73e3eea8..2e366282de3 100644
--- a/app/assets/javascripts/design_management/components/design_notes/toggle_replies_widget.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/toggle_replies_widget.vue
@@ -52,18 +52,18 @@ export default {
{{ toggleText }}
</gl-button>
<template v-if="collapsed">
- <span class="gl-text-gray-700">{{ __('Last reply by') }}</span>
+ <span class="gl-text-gray-500">{{ __('Last reply by') }}</span>
<gl-link
:href="lastReply.author.webUrl"
target="_blank"
- class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2"
+ class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2"
>
{{ lastReply.author.name }}
</gl-link>
<time-ago-tooltip
:time="lastReply.createdAt"
tooltip-placement="bottom"
- class="gl-text-gray-700"
+ class="gl-text-gray-500"
/>
</template>
</li>
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index 333ad2557e8..e5a3590877e 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -1,8 +1,8 @@
<script>
-import { s__ } from '~/locale';
import Cookies from 'js-cookie';
-import { parseBoolean } from '~/lib/utils/common_utils';
import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { parseBoolean } from '~/lib/utils/common_utils';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import { extractDiscussions, extractParticipants } from '../utils/design_management_utils';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
@@ -48,7 +48,7 @@ export default {
};
},
discussionParticipants() {
- return extractParticipants(this.issue.participants);
+ return extractParticipants(this.issue.participants.nodes);
},
resolvedDiscussions() {
return this.discussions.filter(discussion => discussion.resolved);
@@ -94,7 +94,7 @@ export default {
{{ issue.title }}
</h2>
<a
- class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
+ class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
:href="issue.webUrl"
>{{ issue.webPath }}</a
>
@@ -132,7 +132,7 @@ export default {
data-testid="resolved-comments"
:icon="resolvedCommentsToggleIcon"
variant="link"
- class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-mb-4"
+ class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
@click="$emit('toggleResolvedComments')"
>{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }})
</gl-button>
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index eaa641d85d6..292b6e09055 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -74,7 +74,7 @@ export default {
deletion: {
name: 'file-deletion-solid',
classes: 'text-danger-500',
- tooltip: __('Deleted in this version'),
+ tooltip: __('Archived in this version'),
},
};
@@ -127,10 +127,10 @@ export default {
params: { id: filename },
query: $route.query,
}"
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
>
<div class="card-body p-0 d-flex-center overflow-hidden position-relative">
- <div v-if="icon.name" class="design-event position-absolute">
+ <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute">
<span :title="icon.tooltip" :aria-label="icon.tooltip">
<icon :name="icon.name" :size="18" :class="icon.classes" />
</span>
diff --git a/app/assets/javascripts/design_management_new/components/toolbar/pagination.vue b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
index bf62a8f66a6..afca8ed2c6f 100644
--- a/app/assets/javascripts/design_management_new/components/toolbar/pagination.vue
+++ b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
@@ -1,14 +1,15 @@
<script>
/* global Mousetrap */
import 'mousetrap';
+import { GlButton, GlButtonGroup } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
-import PaginationButton from './pagination_button.vue';
import allDesignsMixin from '../../mixins/all_designs';
import { DESIGN_ROUTE_NAME } from '../../router/constants';
export default {
components: {
- PaginationButton,
+ GlButton,
+ GlButtonGroup,
},
mixins: [allDesignsMixin],
props: {
@@ -31,12 +32,12 @@ export default {
});
},
previousDesign() {
- if (!this.designsCount) return null;
+ if (this.currentIndex === 0) return null;
return this.designs[this.currentIndex - 1];
},
nextDesign() {
- if (!this.designsCount) return null;
+ if (this.currentIndex + 1 === this.designsCount) return null;
return this.designs[this.currentIndex + 1];
},
@@ -65,19 +66,21 @@ export default {
<template>
<div v-if="designsCount" class="d-flex align-items-center">
{{ paginationText }}
- <div class="btn-group ml-3 mr-3">
- <pagination-button
- :design="previousDesign"
+ <gl-button-group class="ml-3 mr-3">
+ <gl-button
+ :disabled="!previousDesign"
:title="s__('DesignManagement|Go to previous design')"
- icon-name="angle-left"
+ icon="angle-left"
class="js-previous-design"
+ @click="navigateToDesign(previousDesign)"
/>
- <pagination-button
- :design="nextDesign"
+ <gl-button
+ :disabled="!nextDesign"
:title="s__('DesignManagement|Go to next design')"
- icon-name="angle-right"
+ icon="angle-right"
class="js-next-design"
+ @click="navigateToDesign(nextDesign)"
/>
- </div>
+ </gl-button-group>
</div>
</template>
diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue
index b998dfc47b8..a1cb57123ab 100644
--- a/app/assets/javascripts/design_management/components/toolbar/index.vue
+++ b/app/assets/javascripts/design_management/components/toolbar/index.vue
@@ -1,20 +1,18 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
-import Pagination from './pagination.vue';
+import DesignNavigation from './design_navigation.vue';
import DeleteButton from '../delete_button.vue';
import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
-import appDataQuery from '../../graphql/queries/app_data.query.graphql';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
export default {
components: {
- Icon,
- Pagination,
+ GlButton,
+ GlIcon,
+ DesignNavigation,
DeleteButton,
- GlDeprecatedButton,
},
mixins: [timeagoMixin],
props: {
@@ -55,19 +53,17 @@ export default {
permissions: {
createDesign: false,
},
- projectPath: '',
- issueIid: null,
};
},
- apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
+ inject: {
+ projectPath: {
+ default: '',
},
+ issueIid: {
+ default: '',
+ },
+ },
+ apollo: {
permissions: {
query: permissionsQuery,
variables() {
@@ -95,32 +91,36 @@ export default {
</script>
<template>
- <header class="d-flex p-2 bg-white align-items-center js-design-header">
- <router-link
- :to="{
- name: $options.DESIGNS_ROUTE_NAME,
- query: $route.query,
- }"
- :aria-label="s__('DesignManagement|Go back to designs')"
- class="mr-3 text-plain d-flex justify-content-center align-items-center"
- >
- <icon :size="18" name="close" />
- </router-link>
- <div class="overflow-hidden d-flex align-items-center">
- <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2>
- <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small>
+ <header
+ class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-bg-white gl-py-4 gl-pl-4 js-design-header"
+ >
+ <div class="gl-display-flex gl-align-items-center">
+ <router-link
+ :to="{
+ name: $options.DESIGNS_ROUTE_NAME,
+ query: $route.query,
+ }"
+ :aria-label="s__('DesignManagement|Go back to designs')"
+ data-testid="close-design"
+ class="gl-mr-5 gl-display-flex gl-align-items-center gl-justify-content-center text-plain"
+ >
+ <gl-icon name="close" />
+ </router-link>
+ <div class="overflow-hidden d-flex align-items-center">
+ <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2>
+ <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small>
+ </div>
</div>
- <pagination :id="id" class="ml-auto flex-shrink-0" />
- <gl-deprecated-button :href="image" class="mr-2">
- <icon :size="18" name="download" />
- </gl-deprecated-button>
+ <design-navigation :id="id" class="ml-auto flex-shrink-0" />
+ <gl-button :href="image" icon="download" />
<delete-button
v-if="isLatestVersion && canDeleteDesign"
+ class="gl-ml-3"
:is-deleting="isDeleting"
- button-variant="danger"
+ button-variant="warning"
+ button-icon="archive"
+ button-category="secondary"
@deleteSelectedDesigns="$emit('delete')"
- >
- <icon :size="18" name="remove" />
- </delete-button>
+ />
</header>
</template>
diff --git a/app/assets/javascripts/design_management/components/upload/button.vue b/app/assets/javascripts/design_management/components/upload/button.vue
index 68555104a3c..c76041c74a8 100644
--- a/app/assets/javascripts/design_management/components/upload/button.vue
+++ b/app/assets/javascripts/design_management/components/upload/button.vue
@@ -1,10 +1,10 @@
<script>
-import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
},
directives: {
@@ -30,7 +30,7 @@ export default {
<template>
<div>
- <gl-deprecated-button
+ <gl-button
v-gl-tooltip.hover
:title="
s__(
@@ -38,12 +38,13 @@ export default {
)
"
:disabled="isSaving"
- variant="success"
+ variant="default"
+ size="small"
@click="openFileUpload"
>
{{ s__('DesignManagement|Upload designs') }}
<gl-loading-icon v-if="isSaving" inline class="ml-1" />
- </gl-deprecated-button>
+ </gl-button>
<input
ref="fileUpload"
diff --git a/app/assets/javascripts/design_management/components/upload/design_dropzone.vue b/app/assets/javascripts/design_management/components/upload/design_dropzone.vue
index 33261134c15..7254b7cd16a 100644
--- a/app/assets/javascripts/design_management/components/upload/design_dropzone.vue
+++ b/app/assets/javascripts/design_management/components/upload/design_dropzone.vue
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import uploadDesignMutation from '../../graphql/mutations/upload_design.mutation.graphql';
import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR } from '../../utils/error_messages';
import { isValidDesignFile } from '../../utils/design_management_utils';
@@ -12,6 +12,17 @@ export default {
GlLink,
GlSprintf,
},
+ props: {
+ hasDesigns: {
+ type: Boolean,
+ required: true,
+ },
+ isDraggingDesign: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
data() {
return {
dragCounter: 0,
@@ -22,6 +33,12 @@ export default {
dragging() {
return this.dragCounter !== 0;
},
+ iconStyles() {
+ return {
+ size: this.hasDesigns ? 24 : 16,
+ class: this.hasDesigns ? 'gl-mb-2' : 'gl-mr-3 gl-text-gray-500',
+ };
+ },
},
methods: {
isValidUpload(files) {
@@ -76,25 +93,21 @@ export default {
>
<slot>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
@click="openFileUpload"
>
- <div class="d-flex-center flex-column text-center">
- <gl-icon name="doc-new" :size="48" class="mb-4" />
- <p>
- <gl-sprintf
- :message="
- __(
- '%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}.',
- )
- "
- >
- <template #lineOne="{ content }"
- ><span class="d-block">{{ content }}</span>
- </template>
-
+ <div
+ :class="{ 'gl-flex-direction-column': hasDesigns }"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center"
+ data-testid="dropzone-area"
+ >
+ <gl-icon name="upload" :size="iconStyles.size" :class="iconStyles.class" />
+ <p class="gl-mb-0">
+ <gl-sprintf :message="__('Drop or %{linkStart}upload%{linkEnd} designs to attach')">
<template #link="{ content }">
- <gl-link class="h-100 w-100" @click.stop="openFileUpload">{{ content }}</gl-link>
+ <gl-link @click.stop="openFileUpload">
+ {{ content }}
+ </gl-link>
</template>
</gl-sprintf>
</p>
@@ -113,11 +126,11 @@ export default {
</slot>
<transition name="design-dropzone-fade">
<div
- v-show="dragging"
+ v-show="dragging && !isDraggingDesign"
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
>
<div v-show="!isDragDataValid" class="mw-50 text-center">
- <h3>{{ __('Oh no!') }}</h3>
+ <h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Oh no!') }}</h3>
<span>{{
__(
'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.',
@@ -125,7 +138,7 @@ export default {
}}</span>
</div>
<div v-show="isDragDataValid" class="mw-50 text-center">
- <h3>{{ __('Incoming!') }}</h3>
+ <h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Incoming!') }}</h3>
<span>{{ __('Drop your designs to start your upload.') }}</span>
</div>
</div>
diff --git a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
index 993eac6f37f..a03982cb91b 100644
--- a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
+++ b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
@@ -1,13 +1,14 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlNewDropdown, GlNewDropdownItem, GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import allVersionsMixin from '../../mixins/all_versions';
import { findVersionId } from '../../utils/design_management_utils';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
+ GlNewDropdown,
+ GlNewDropdownItem,
+ GlSprintf,
},
mixins: [allVersionsMixin],
computed: {
@@ -18,7 +19,7 @@ export default {
if (!this.queryVersion) return 0;
const idx = this.allVersions.findIndex(
- version => this.findVersionId(version.node.id) === this.queryVersion,
+ version => this.findVersionId(version.id) === this.queryVersion,
);
// if the currentVersionId isn't a valid version (i.e. not in allVersions)
@@ -29,48 +30,52 @@ export default {
if (this.queryVersion) return this.queryVersion;
const currentVersion = this.allVersions[this.currentVersionIdx];
- return this.findVersionId(currentVersion.node.id);
+ return this.findVersionId(currentVersion.id);
},
dropdownText() {
if (this.isLatestVersion) {
- return __('Showing Latest Version');
+ return __('Showing latest version');
}
// allVersions is sorted in reverse chronological order (latest first)
const currentVersionNumber = this.allVersions.length - this.currentVersionIdx;
- return sprintf(__('Showing Version #%{versionNumber}'), {
+ return sprintf(__('Showing version #%{versionNumber}'), {
versionNumber: currentVersionNumber,
});
},
},
methods: {
findVersionId,
+ routeToVersion(versionId) {
+ this.$router.push({
+ path: this.$route.path,
+ query: { version: this.findVersionId(versionId) },
+ });
+ },
+ versionText(versionId) {
+ if (this.findVersionId(versionId) === this.latestVersionId) {
+ return __('Version %{versionNumber} (latest)');
+ }
+ return __('Version %{versionNumber}');
+ },
},
};
</script>
<template>
- <gl-dropdown :text="dropdownText" variant="link" class="design-version-dropdown">
- <gl-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id">
- <router-link
- class="d-flex js-version-link"
- :to="{ path: $route.path, query: { version: findVersionId(version.node.id) } }"
- >
- <div class="flex-grow-1 ml-2">
- <div>
- <strong
- >{{ __('Version') }} {{ allVersions.length - index }}
- <span v-if="findVersionId(version.node.id) === latestVersionId"
- >({{ __('latest') }})</span
- >
- </strong>
- </div>
- </div>
- <i
- v-if="findVersionId(version.node.id) === currentVersionId"
- class="fa fa-check pull-right"
- ></i>
- </router-link>
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-new-dropdown :text="dropdownText" size="small">
+ <gl-new-dropdown-item
+ v-for="(version, index) in allVersions"
+ :key="version.id"
+ :is-check-item="true"
+ :is-checked="findVersionId(version.id) === currentVersionId"
+ @click="routeToVersion(version.id)"
+ >
+ <gl-sprintf :message="versionText(version.id)">
+ <template #versionNumber>
+ {{ allVersions.length - index }}
+ </template>
+ </gl-sprintf>
+ </gl-new-dropdown-item>
+ </gl-new-dropdown>
</template>
diff --git a/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql
index c8ade328120..0b8400ac040 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/create_image_diff_note.mutation.graphql
@@ -8,10 +8,8 @@ mutation createImageDiffNote($input: CreateImageDiffNoteInput!) {
id
replyId
notes {
- edges {
- node {
- ...DesignNote
- }
+ nodes {
+ ...DesignNote
}
}
}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/move_design.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/move_design.mutation.graphql
new file mode 100644
index 00000000000..144b2729999
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/move_design.mutation.graphql
@@ -0,0 +1,18 @@
+#import "../fragments/design_list.fragment.graphql"
+
+mutation DesignManagementMove(
+ $id: DesignManagementDesignID!
+ $previous: DesignManagementDesignID
+ $next: DesignManagementDesignID
+) {
+ designManagementMove(input: { id: $id, previous: $previous, next: $next }) {
+ designCollection {
+ designs {
+ nodes {
+ ...DesignListItem
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
index d694e6558a0..84aeb374351 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
@@ -5,11 +5,9 @@ mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
designs {
...DesignItem
versions {
- edges {
- node {
- id
- sha
- }
+ nodes {
+ id
+ sha
}
}
}
diff --git a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
index 07a9af55787..ab987dda525 100644
--- a/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
+++ b/app/assets/javascripts/design_management/graphql/queries/get_design.query.graphql
@@ -7,19 +7,15 @@ query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [Stri
issue(iid: $iid) {
designCollection {
designs(atVersion: $atVersion, filenames: $filenames) {
- edges {
- node {
- ...DesignItem
- issue {
- title
- webPath
- webUrl
- participants {
- edges {
- node {
- ...Author
- }
- }
+ nodes {
+ ...DesignItem
+ issue {
+ title
+ webPath
+ webUrl
+ participants {
+ nodes {
+ ...Author
}
}
}
diff --git a/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql b/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql
index 121a50555b3..96efa8e8242 100644
--- a/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql
+++ b/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql
@@ -7,17 +7,13 @@ query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) {
issue(iid: $iid) {
designCollection {
designs(atVersion: $atVersion) {
- edges {
- node {
- ...DesignListItem
- }
+ nodes {
+ ...DesignListItem
}
}
versions {
- edges {
- node {
- ...VersionListItem
- }
+ nodes {
+ ...VersionListItem
}
}
}
diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js
index 1fc5779515a..20c9cacf83f 100644
--- a/app/assets/javascripts/design_management/index.js
+++ b/app/assets/javascripts/design_management/index.js
@@ -1,32 +1,15 @@
-// This application is being moved, please do not touch this files
-// Please see https://gitlab.com/gitlab-org/gitlab/-/issues/14744#note_364468096 for details
-
-import $ from 'jquery';
import Vue from 'vue';
import createRouter from './router';
import App from './components/app.vue';
import apolloProvider from './graphql';
-import getDesignListQuery from './graphql/queries/get_design_list.query.graphql';
-import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
export default () => {
- const el = document.querySelector('.js-design-management');
- const badge = document.querySelector('.js-designs-count');
+ const el = document.querySelector('.js-design-management-new');
const { issueIid, projectPath, issuePath } = el.dataset;
const router = createRouter(issuePath);
- $('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => {
- if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) {
- router.push({ name: DESIGNS_ROUTE_NAME });
- } else if (id === 'discussion') {
- router.push({ name: ROOT_ROUTE_NAME });
- }
- });
-
apolloProvider.clients.defaultClient.cache.writeData({
data: {
- projectPath,
- issueIid,
activeDiscussion: {
__typename: 'ActiveDiscussion',
id: null,
@@ -35,25 +18,14 @@ export default () => {
},
});
- apolloProvider.clients.defaultClient
- .watchQuery({
- query: getDesignListQuery,
- variables: {
- fullPath: projectPath,
- iid: issueIid,
- atVersion: null,
- },
- })
- .subscribe(({ data }) => {
- if (badge) {
- badge.textContent = data.project.issue.designCollection.designs.edges.length;
- }
- });
-
return new Vue({
el,
router,
apolloProvider,
+ provide: {
+ projectPath,
+ issueIid,
+ },
render(createElement) {
return createElement(App);
},
diff --git a/app/assets/javascripts/design_management/mixins/all_designs.js b/app/assets/javascripts/design_management/mixins/all_designs.js
index f7d6551c46c..0c2858bb14b 100644
--- a/app/assets/javascripts/design_management/mixins/all_designs.js
+++ b/app/assets/javascripts/design_management/mixins/all_designs.js
@@ -1,8 +1,7 @@
import { propertyOf } from 'lodash';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import { extractNodes } from '../utils/design_management_utils';
import allVersionsMixin from './all_versions';
import { DESIGNS_ROUTE_NAME } from '../router/constants';
@@ -19,9 +18,15 @@ export default {
};
},
update: data => {
- const designEdges = propertyOf(data)(['project', 'issue', 'designCollection', 'designs']);
- if (designEdges) {
- return extractNodes(designEdges);
+ const designNodes = propertyOf(data)([
+ 'project',
+ 'issue',
+ 'designCollection',
+ 'designs',
+ 'nodes',
+ ]);
+ if (designNodes) {
+ return designNodes;
}
return [];
},
diff --git a/app/assets/javascripts/design_management/mixins/all_versions.js b/app/assets/javascripts/design_management/mixins/all_versions.js
index 3966fe71732..7a094f23378 100644
--- a/app/assets/javascripts/design_management/mixins/all_versions.js
+++ b/app/assets/javascripts/design_management/mixins/all_versions.js
@@ -1,17 +1,8 @@
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
-import appDataQuery from '../graphql/queries/app_data.query.graphql';
import { findVersionId } from '../utils/design_management_utils';
export default {
apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
- },
allVersions: {
query: getDesignListQuery,
variables() {
@@ -21,7 +12,15 @@ export default {
atVersion: null,
};
},
- update: data => data.project.issue.designCollection.versions.edges,
+ update: data => data.project.issue.designCollection.versions.nodes,
+ },
+ },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ issueIid: {
+ default: '',
},
},
computed: {
@@ -29,7 +28,7 @@ export default {
return (
this.$route.query.version &&
this.allVersions &&
- this.allVersions.some(version => version.node.id.endsWith(this.$route.query.version))
+ this.allVersions.some(version => version.id.endsWith(this.$route.query.version))
);
},
designsVersion() {
@@ -39,7 +38,7 @@ export default {
},
latestVersionId() {
const latestVersion = this.allVersions[0];
- return latestVersion && findVersionId(latestVersion.node.id);
+ return latestVersion && findVersionId(latestVersion.id);
},
isLatestVersion() {
if (this.allVersions.length > 0) {
@@ -55,8 +54,6 @@ export default {
data() {
return {
allVersions: [],
- projectPath: '',
- issueIid: null,
};
},
};
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 9a959222e22..17b72e73127 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -2,7 +2,7 @@
import Mousetrap from 'mousetrap';
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import allVersionsMixin from '../../mixins/all_versions';
import Toolbar from '../../components/toolbar/index.vue';
@@ -12,7 +12,6 @@ import DesignPresentation from '../../components/design_presentation.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignSidebar from '../../components/design_sidebar.vue';
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
-import appDataQuery from '../../graphql/queries/app_data.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql';
import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
@@ -62,22 +61,12 @@ export default {
design: {},
comment: '',
annotationCoordinates: null,
- projectPath: '',
errorMessage: '',
- issueIid: '',
scale: 1,
resolvedDiscussionsExpanded: false,
};
},
apollo: {
- appData: {
- query: appDataQuery,
- manual: true,
- result({ data: { projectPath, issueIid } }) {
- this.projectPath = projectPath;
- this.issueIid = issueIid;
- },
- },
design: {
query: getDesignQuery,
// We want to see cached design version if we have one, and fetch newer version on the background to update discussions
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index d14a1fc8c1c..cd68e9d6c5b 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -1,6 +1,7 @@
<script>
-import { GlLoadingIcon, GlDeprecatedButton, GlAlert } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
+import VueDraggable from 'vuedraggable';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
import UploadButton from '../components/upload/button.vue';
import DeleteButton from '../components/delete_button.vue';
@@ -9,6 +10,7 @@ import DesignDestroyer from '../components/design_destroyer.vue';
import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue';
import DesignDropzone from '../components/upload/design_dropzone.vue';
import uploadDesignMutation from '../graphql/mutations/upload_design.mutation.graphql';
+import moveDesignMutation from '../graphql/mutations/move_design.mutation.graphql';
import permissionsQuery from '../graphql/queries/design_permissions.query.graphql';
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
import allDesignsMixin from '../mixins/all_designs';
@@ -16,13 +18,18 @@ import {
UPLOAD_DESIGN_ERROR,
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
+ MOVE_DESIGN_ERROR,
designUploadSkippedWarning,
designDeletionError,
} from '../utils/error_messages';
-import { updateStoreAfterUploadDesign } from '../utils/cache_update';
+import {
+ updateStoreAfterUploadDesign,
+ updateDesignsOnStoreAfterReorder,
+} from '../utils/cache_update';
import {
designUploadOptimisticResponse,
isValidDesignFile,
+ moveDesignOptimisticResponse,
} from '../utils/design_management_utils';
import { getFilename } from '~/lib/utils/file_upload';
import { DESIGNS_ROUTE_NAME } from '../router/constants';
@@ -33,13 +40,14 @@ export default {
components: {
GlLoadingIcon,
GlAlert,
- GlDeprecatedButton,
+ GlButton,
UploadButton,
Design,
DesignDestroyer,
DesignVersionDropdown,
DeleteButton,
DesignDropzone,
+ VueDraggable,
},
mixins: [allDesignsMixin],
apollo: {
@@ -61,6 +69,8 @@ export default {
},
filesToBeSaved: [],
selectedDesigns: [],
+ isDraggingDesign: false,
+ reorderedDesigns: null,
};
},
computed: {
@@ -96,9 +106,19 @@ export default {
? s__('DesignManagement|Deselect all')
: s__('DesignManagement|Select all');
},
+ isDesignListEmpty() {
+ return !this.isSaving && !this.hasDesigns;
+ },
+ designDropzoneWrapperClass() {
+ return this.isDesignListEmpty
+ ? 'col-12'
+ : 'gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3';
+ },
},
mounted() {
- this.toggleOnPasteListener(this.$route.name);
+ if (this.$route.path === '/designs') {
+ this.$el.scrollIntoView();
+ }
},
methods: {
resetFilesToBeSaved() {
@@ -238,56 +258,97 @@ export default {
this.onUploadDesign([newFile]);
}
},
- toggleOnPasteListener(route) {
- if (route === DESIGNS_ROUTE_NAME) {
- document.addEventListener('paste', this.onDesignPaste);
- } else {
- document.removeEventListener('paste', this.onDesignPaste);
+ toggleOnPasteListener() {
+ document.addEventListener('paste', this.onDesignPaste);
+ },
+ toggleOffPasteListener() {
+ document.removeEventListener('paste', this.onDesignPaste);
+ },
+ designMoveVariables(newIndex, element) {
+ const variables = {
+ id: element.id,
+ };
+ if (newIndex > 0) {
+ variables.previous = this.reorderedDesigns[newIndex - 1].id;
+ }
+ if (newIndex < this.reorderedDesigns.length - 1) {
+ variables.next = this.reorderedDesigns[newIndex + 1].id;
}
+ return variables;
+ },
+ reorderDesigns({ moved: { newIndex, element } }) {
+ this.$apollo
+ .mutate({
+ mutation: moveDesignMutation,
+ variables: this.designMoveVariables(newIndex, element),
+ update: (store, { data: { designManagementMove } }) => {
+ return updateDesignsOnStoreAfterReorder(
+ store,
+ designManagementMove,
+ this.projectQueryBody,
+ );
+ },
+ optimisticResponse: moveDesignOptimisticResponse(this.reorderedDesigns),
+ })
+ .catch(() => {
+ createFlash(MOVE_DESIGN_ERROR);
+ });
+ },
+ onDesignMove(designs) {
+ this.reorderedDesigns = designs;
},
},
beforeRouteUpdate(to, from, next) {
- this.toggleOnPasteListener(to.name);
this.selectedDesigns = [];
next();
},
- beforeRouteLeave(to, from, next) {
- this.toggleOnPasteListener(to.name);
- next();
+ dragOptions: {
+ animation: 200,
+ ghostClass: 'gl-visibility-hidden',
},
};
</script>
<template>
- <div>
+ <div
+ data-testid="designs-root"
+ class="gl-mt-5"
+ @mouseenter="toggleOnPasteListener"
+ @mouseleave="toggleOffPasteListener"
+ >
<header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
- <div class="d-flex justify-content-between align-items-center w-100">
- <design-version-dropdown />
- <div :class="['qa-selector-toolbar', { 'd-flex': hasDesigns, 'd-none': !hasDesigns }]">
- <gl-deprecated-button
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full">
+ <div>
+ <span class="gl-font-weight-bold gl-mr-3">{{ s__('DesignManagement|Designs') }}</span>
+ <design-version-dropdown />
+ </div>
+ <div v-show="hasDesigns" class="qa-selector-toolbar gl-display-flex gl-align-items-center">
+ <gl-button
v-if="isLatestVersion"
variant="link"
- class="mr-2 js-select-all"
+ size="small"
+ class="gl-mr-3 js-select-all"
@click="toggleDesignsSelection"
- >{{ selectAllButtonText }}</gl-deprecated-button
- >
+ >{{ selectAllButtonText }}
+ </gl-button>
<design-destroyer
#default="{ mutate, loading }"
:filenames="selectedDesigns"
- :project-path="projectPath"
- :iid="issueIid"
@done="onDesignDelete"
@error="onDesignDeleteError"
>
<delete-button
v-if="isLatestVersion"
:is-deleting="loading"
- button-class="btn-danger btn-inverted mr-2"
+ button-variant="warning"
+ button-category="secondary"
+ button-class="gl-mr-3"
+ button-size="small"
+ :loading="loading"
:has-selected-designs="hasSelectedDesigns"
@deleteSelectedDesigns="mutate()"
>
- {{ s__('DesignManagement|Delete selected') }}
- <gl-loading-icon v-if="loading" inline class="ml-1" />
+ {{ s__('DesignManagement|Archive selected') }}
</delete-button>
</design-destroyer>
<upload-button v-if="canCreateDesign" :is-saving="isSaving" @upload="onUploadDesign" />
@@ -299,14 +360,35 @@ export default {
<gl-alert v-else-if="error" variant="danger" :dismissible="false">
{{ __('An error occurred while loading designs. Please try again.') }}
</gl-alert>
- <ol v-else class="list-unstyled row">
- <li class="col-md-6 col-lg-4 mb-3">
- <design-dropzone class="design-list-item" @change="onUploadDesign" />
- </li>
- <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3">
- <design-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)"
- ><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)"
- /></design-dropzone>
+ <vue-draggable
+ v-else
+ :value="designs"
+ :disabled="!isLatestVersion"
+ v-bind="$options.dragOptions"
+ tag="ol"
+ draggable=".js-design-tile"
+ class="list-unstyled row"
+ @start="isDraggingDesign = true"
+ @end="isDraggingDesign = false"
+ @change="reorderDesigns"
+ @input="onDesignMove"
+ >
+ <li
+ v-for="design in designs"
+ :key="design.id"
+ class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
+ >
+ <design-dropzone
+ :has-designs="hasDesigns"
+ :is-dragging-design="isDraggingDesign"
+ @change="onExistingDesignDropzoneChange($event, design.filename)"
+ >
+ <design
+ v-bind="design"
+ :is-uploading="isDesignToBeSaved(design.filename)"
+ class="gl-bg-white"
+ />
+ </design-dropzone>
<input
v-if="canSelectDesign(design.filename)"
@@ -316,7 +398,17 @@ export default {
@change="changeSelectedDesigns(design.filename)"
/>
</li>
- </ol>
+ <template #header>
+ <li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper">
+ <design-dropzone
+ :is-dragging-design="isDraggingDesign"
+ :class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }"
+ :has-designs="hasDesigns"
+ @change="onUploadDesign"
+ />
+ </li>
+ </template>
+ </vue-draggable>
</div>
<router-view :key="$route.fullPath" />
</div>
diff --git a/app/assets/javascripts/design_management/router/constants.js b/app/assets/javascripts/design_management/router/constants.js
index abeef520e33..dd2ee8d8689 100644
--- a/app/assets/javascripts/design_management/router/constants.js
+++ b/app/assets/javascripts/design_management/router/constants.js
@@ -1,3 +1,2 @@
-export const ROOT_ROUTE_NAME = 'root';
export const DESIGNS_ROUTE_NAME = 'designs';
export const DESIGN_ROUTE_NAME = 'design';
diff --git a/app/assets/javascripts/design_management/router/index.js b/app/assets/javascripts/design_management/router/index.js
index 7494da002c8..cbeb2f7ce42 100644
--- a/app/assets/javascripts/design_management/router/index.js
+++ b/app/assets/javascripts/design_management/router/index.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './routes';
@@ -16,9 +15,7 @@ export default function createRouter(base) {
});
const pageEl = getPageLayoutElement();
- router.beforeEach(({ meta: { el }, name }, _, next) => {
- $(`#${el}`).tab('show');
-
+ router.beforeEach(({ name }, _, next) => {
// apply a fullscreen layout style in Design View (a.k.a design detail)
if (pageEl) {
if (name === DESIGN_ROUTE_NAME) {
diff --git a/app/assets/javascripts/design_management/router/routes.js b/app/assets/javascripts/design_management/router/routes.js
index 788910e5514..d888b856611 100644
--- a/app/assets/javascripts/design_management/router/routes.js
+++ b/app/assets/javascripts/design_management/router/routes.js
@@ -1,44 +1,29 @@
import Home from '../pages/index.vue';
import DesignDetail from '../pages/design/index.vue';
-import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
+import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
export default [
{
- name: ROOT_ROUTE_NAME,
+ name: DESIGNS_ROUTE_NAME,
path: '/',
component: Home,
- meta: {
- el: 'discussion',
- },
+ alias: '/designs',
},
{
- name: DESIGNS_ROUTE_NAME,
- path: '/designs',
- component: Home,
- meta: {
- el: 'designs',
- },
- children: [
+ name: DESIGN_ROUTE_NAME,
+ path: '/designs/:id',
+ component: DesignDetail,
+ beforeEnter(
{
- name: DESIGN_ROUTE_NAME,
- path: ':id',
- component: DesignDetail,
- meta: {
- el: 'designs',
- },
- beforeEnter(
- {
- params: { id },
- },
- from,
- next,
- ) {
- if (typeof id === 'string') {
- next();
- }
- },
- props: ({ params: { id } }) => ({ id }),
+ params: { id },
},
- ],
+ _,
+ next,
+ ) {
+ if (typeof id === 'string') {
+ next();
+ }
+ },
+ props: ({ params: { id } }) => ({ id }),
},
];
diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js
index 24b374b79fd..b79df9d01d5 100644
--- a/app/assets/javascripts/design_management/utils/cache_update.js
+++ b/app/assets/javascripts/design_management/utils/cache_update.js
@@ -1,6 +1,7 @@
/* eslint-disable @gitlab/require-i18n-strings */
-import createFlash from '~/flash';
+import { groupBy } from 'lodash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { extractCurrentDiscussion, extractDesign } from './design_management_utils';
import {
ADD_IMAGE_DIFF_NOTE_ERROR,
@@ -12,10 +13,10 @@ import {
const deleteDesignsFromStore = (store, query, selectedDesigns) => {
const data = store.readQuery(query);
- const changedDesigns = data.project.issue.designCollection.designs.edges.filter(
- ({ node }) => !selectedDesigns.includes(node.filename),
+ const changedDesigns = data.project.issue.designCollection.designs.nodes.filter(
+ node => !selectedDesigns.includes(node.filename),
);
- data.project.issue.designCollection.designs.edges = [...changedDesigns];
+ data.project.issue.designCollection.designs.nodes = [...changedDesigns];
store.writeQuery({
...query,
@@ -34,11 +35,10 @@ const addNewVersionToStore = (store, query, version) => {
if (!version) return;
const data = store.readQuery(query);
- const newEdge = { node: version, __typename: 'DesignVersionEdge' };
- data.project.issue.designCollection.versions.edges = [
- newEdge,
- ...data.project.issue.designCollection.versions.edges,
+ data.project.issue.designCollection.versions.nodes = [
+ version,
+ ...data.project.issue.designCollection.versions.nodes,
];
store.writeQuery({
@@ -59,18 +59,15 @@ const addDiscussionCommentToStore = (store, createNote, query, queryVariables, d
design.notesCount += 1;
if (
- !design.issue.participants.edges.some(
- participant => participant.node.username === createNote.note.author.username,
+ !design.issue.participants.nodes.some(
+ participant => participant.username === createNote.note.author.username,
)
) {
- design.issue.participants.edges = [
- ...design.issue.participants.edges,
+ design.issue.participants.nodes = [
+ ...design.issue.participants.nodes,
{
- __typename: 'UserEdge',
- node: {
- __typename: 'User',
- ...createNote.note.author,
- },
+ __typename: 'User',
+ ...createNote.note.author,
},
];
}
@@ -108,18 +105,15 @@ const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) =
const notesCount = design.notesCount + 1;
design.discussions.nodes = [...design.discussions.nodes, newDiscussion];
if (
- !design.issue.participants.edges.some(
- participant => participant.node.username === createImageDiffNote.note.author.username,
+ !design.issue.participants.nodes.some(
+ participant => participant.username === createImageDiffNote.note.author.username,
)
) {
- design.issue.participants.edges = [
- ...design.issue.participants.edges,
+ design.issue.participants.nodes = [
+ ...design.issue.participants.nodes,
{
- __typename: 'UserEdge',
- node: {
- __typename: 'User',
- ...createImageDiffNote.note.author,
- },
+ __typename: 'User',
+ ...createImageDiffNote.note.author,
},
];
}
@@ -166,42 +160,37 @@ const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables
const addNewDesignToStore = (store, designManagementUpload, query) => {
const data = store.readQuery(query);
- const newDesigns = data.project.issue.designCollection.designs.edges.reduce((acc, design) => {
- if (!acc.find(d => d.filename === design.node.filename)) {
- acc.push(design.node);
- }
-
- return acc;
- }, designManagementUpload.designs);
+ const currentDesigns = data.project.issue.designCollection.designs.nodes;
+ const existingDesigns = groupBy(currentDesigns, 'filename');
+ const newDesigns = currentDesigns.concat(
+ designManagementUpload.designs.filter(d => !existingDesigns[d.filename]),
+ );
let newVersionNode;
const findNewVersions = designManagementUpload.designs.find(design => design.versions);
if (findNewVersions) {
- const findNewVersionsEdges = findNewVersions.versions.edges;
+ const findNewVersionsNodes = findNewVersions.versions.nodes;
- if (findNewVersionsEdges && findNewVersionsEdges.length) {
- newVersionNode = [findNewVersionsEdges[0]];
+ if (findNewVersionsNodes && findNewVersionsNodes.length) {
+ newVersionNode = [findNewVersionsNodes[0]];
}
}
const newVersions = [
...(newVersionNode || []),
- ...data.project.issue.designCollection.versions.edges,
+ ...data.project.issue.designCollection.versions.nodes,
];
const updatedDesigns = {
__typename: 'DesignCollection',
designs: {
__typename: 'DesignConnection',
- edges: newDesigns.map(design => ({
- __typename: 'DesignEdge',
- node: design,
- })),
+ nodes: newDesigns,
},
versions: {
__typename: 'DesignVersionConnection',
- edges: newVersions,
+ nodes: newVersions,
},
};
@@ -213,6 +202,15 @@ const addNewDesignToStore = (store, designManagementUpload, query) => {
});
};
+const moveDesignInStore = (store, designManagementMove, query) => {
+ const data = store.readQuery(query);
+ data.project.issue.designCollection.designs = designManagementMove.designCollection.designs;
+ store.writeQuery({
+ ...query,
+ data,
+ });
+};
+
const onError = (data, message) => {
createFlash(message);
throw new Error(data.errors);
@@ -274,3 +272,11 @@ export const updateStoreAfterUploadDesign = (store, data, query) => {
addNewDesignToStore(store, data, query);
}
};
+
+export const updateDesignsOnStoreAfterReorder = (store, data, query) => {
+ if (hasErrors(data)) {
+ createFlash(data.errors[0]);
+ } else {
+ moveDesignInStore(store, data, query);
+ }
+};
diff --git a/app/assets/javascripts/design_management/utils/design_management_utils.js b/app/assets/javascripts/design_management/utils/design_management_utils.js
index 22705cf67a1..da8f89ff960 100644
--- a/app/assets/javascripts/design_management/utils/design_management_utils.js
+++ b/app/assets/javascripts/design_management/utils/design_management_utils.js
@@ -5,17 +5,7 @@ export const isValidDesignFile = ({ type }) =>
(type.match(VALID_DESIGN_FILE_MIMETYPE.regex) || []).length > 0;
/**
- * Returns formatted array that doesn't contain
- * `edges`->`node` nesting
- *
- * @param {Array} elements
- */
-
-export const extractNodes = elements => elements.edges.map(({ node }) => node);
-
-/**
- * Returns formatted array of discussions that doesn't contain
- * `edges`->`node` nesting for child notes
+ * Returns formatted array of discussions
*
* @param {Array} discussions
*/
@@ -40,9 +30,9 @@ export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1];
export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1];
-export const extractDesigns = data => data.project.issue.designCollection.designs.edges;
+export const extractDesigns = data => data.project.issue.designCollection.designs.nodes;
-export const extractDesign = data => (extractDesigns(data) || [])[0]?.node;
+export const extractDesign = data => (extractDesigns(data) || [])[0];
/**
* Generates optimistic response for a design upload mutation
@@ -72,13 +62,10 @@ export const designUploadOptimisticResponse = files => {
},
versions: {
__typename: 'DesignVersionConnection',
- edges: {
- __typename: 'DesignVersionEdge',
- node: {
- __typename: 'DesignVersion',
- id: -uniqueId(),
- sha: -uniqueId(),
- },
+ nodes: {
+ __typename: 'DesignVersion',
+ id: -uniqueId(),
+ sha: -uniqueId(),
},
},
}));
@@ -98,7 +85,8 @@ export const designUploadOptimisticResponse = files => {
/**
* Generates optimistic response for a design upload mutation
- * @param {Array<File>} files
+ * @param {Object} note
+ * @param {Object} position
*/
export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
@@ -117,12 +105,33 @@ export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({
},
});
+/**
+ * Generates optimistic response for a design upload mutation
+ * @param {Array} designs
+ */
+export const moveDesignOptimisticResponse = designs => ({
+ // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Mutation',
+ designManagementMove: {
+ __typename: 'DesignManagementMovePayload',
+ designCollection: {
+ __typename: 'DesignCollection',
+ designs: {
+ __typename: 'DesignConnection',
+ nodes: designs,
+ },
+ },
+ errors: [],
+ },
+});
+
const normalizeAuthor = author => ({
...author,
web_url: author.webUrl,
avatar_url: author.avatarUrl,
});
-export const extractParticipants = users => users.edges.map(({ node }) => normalizeAuthor(node));
+export const extractParticipants = users => users.map(node => normalizeAuthor(node));
export const getPageLayoutElement = () => document.querySelector('.layout-page');
diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js
index 7666c726c2f..c815b11737d 100644
--- a/app/assets/javascripts/design_management/utils/error_messages.js
+++ b/app/assets/javascripts/design_management/utils/error_messages.js
@@ -40,6 +40,10 @@ export const EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE = __(
'You must upload a file with the same file name when dropping onto an existing design.',
);
+export const MOVE_DESIGN_ERROR = __(
+ 'Something went wrong when reordering designs. Please try again',
+);
+
const MAX_SKIPPED_FILES_LISTINGS = 5;
const oneDesignSkippedMessage = filename =>
@@ -69,7 +73,7 @@ const someDesignsSkippedMessage = skippedFiles => {
export const designDeletionError = ({ singular = true } = {}) => {
const design = singular ? __('a design') : __('designs');
- return sprintf(s__('Could not delete %{design}. Please try again.'), {
+ return sprintf(s__('Could not archive %{design}. Please try again.'), {
design,
});
};
diff --git a/app/assets/javascripts/design_management_new/components/app.vue b/app/assets/javascripts/design_management_legacy/components/app.vue
index 98240aef810..98240aef810 100644
--- a/app/assets/javascripts/design_management_new/components/app.vue
+++ b/app/assets/javascripts/design_management_legacy/components/app.vue
diff --git a/app/assets/javascripts/design_management_new/components/delete_button.vue b/app/assets/javascripts/design_management_legacy/components/delete_button.vue
index 77e1b97a227..1fd902c9ed7 100644
--- a/app/assets/javascripts/design_management_new/components/delete_button.vue
+++ b/app/assets/javascripts/design_management_legacy/components/delete_button.vue
@@ -1,12 +1,11 @@
<script>
-import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash';
-import { s__ } from '~/locale';
export default {
name: 'DeleteButton',
components: {
- GlButton,
+ GlDeprecatedButton,
GlModal,
},
directives: {
@@ -26,12 +25,7 @@ export default {
buttonVariant: {
type: String,
required: false,
- default: 'info',
- },
- buttonSize: {
- type: String,
- required: false,
- default: 'medium',
+ default: '',
},
hasSelectedDesigns: {
type: Boolean,
@@ -44,38 +38,27 @@ export default {
modalId: uniqueId('design-deletion-confirmation-'),
};
},
- modal: {
- title: s__('DesignManagement|Delete designs confirmation'),
- actionPrimary: {
- text: s__('Delete'),
- attributes: { variant: 'danger' },
- },
- actionCancel: {
- text: s__('Cancel'),
- },
- },
};
</script>
<template>
- <div class="gl-display-flex gl-align-items-center gl-h-full">
+ <div>
<gl-modal
:modal-id="modalId"
- :title="$options.modal.title"
- :action-primary="$options.modal.actionPrimary"
- :action-cancel="$options.modal.actionCancel"
+ :title="s__('DesignManagement|Delete designs confirmation')"
+ :ok-title="s__('DesignManagement|Delete')"
+ ok-variant="danger"
@ok="$emit('deleteSelectedDesigns')"
>
<p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p>
</gl-modal>
- <gl-button
+ <gl-deprecated-button
v-gl-modal-directive="modalId"
:variant="buttonVariant"
- :size="buttonSize"
- :class="buttonClass"
:disabled="isDeleting || !hasSelectedDesigns"
+ :class="buttonClass"
>
<slot></slot>
- </gl-button>
+ </gl-deprecated-button>
</div>
</template>
diff --git a/app/assets/javascripts/design_management_new/components/design_destroyer.vue b/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue
index 7ae569216f0..62460ca551c 100644
--- a/app/assets/javascripts/design_management_new/components/design_destroyer.vue
+++ b/app/assets/javascripts/design_management_legacy/components/design_destroyer.vue
@@ -13,14 +13,13 @@ export default {
type: Array,
required: true,
},
- },
- inject: {
projectPath: {
- default: '',
+ type: String,
+ required: true,
},
iid: {
- from: 'issueIid',
- defaut: '',
+ type: String,
+ required: true,
},
},
computed: {
diff --git a/app/assets/javascripts/design_management_new/components/design_note_pin.vue b/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue
index 0811397fbad..2b5e62c2870 100644
--- a/app/assets/javascripts/design_management_new/components/design_note_pin.vue
+++ b/app/assets/javascripts/design_management_legacy/components/design_note_pin.vue
@@ -1,11 +1,11 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'DesignNotePin',
components: {
- Icon,
+ GlIcon,
},
props: {
position: {
@@ -47,13 +47,13 @@ export default {
'btn-transparent comment-indicator': isNewNote,
'js-image-badge badge badge-pill': !isNewNote,
}"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center"
+ class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0"
type="button"
@mousedown="$emit('mousedown', $event)"
@mouseup="$emit('mouseup', $event)"
@click="$emit('click', $event)"
>
- <icon v-if="isNewNote" name="image-comment-dark" />
+ <gl-icon v-if="isNewNote" name="image-comment-dark" :size="24" />
<template v-else>
{{ label }}
</template>
diff --git a/app/assets/javascripts/design_management_new/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue
index 4aaf43e3a5b..6a20517eed7 100644
--- a/app/assets/javascripts/design_management_new/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management_legacy/components/design_notes/design_discussion.vue
@@ -230,10 +230,10 @@ export default {
</button>
</template>
<template v-if="discussion.resolved" #resolvedStatus>
- <p class="gl-text-gray-700 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message">
+ <p class="gl-text-gray-500 gl-font-sm gl-m-0 gl-mt-5" data-testid="resolved-message">
{{ __('Resolved by') }}
<gl-link
- class="gl-text-gray-700 gl-text-decoration-none gl-font-sm link-inherit-color"
+ class="gl-text-gray-500 gl-text-decoration-none gl-font-sm link-inherit-color"
:href="discussion.resolvedBy.webUrl"
target="_blank"
>{{ discussion.resolvedBy.name }}</gl-link
diff --git a/app/assets/javascripts/design_management_new/components/design_notes/design_note.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue
index 172e61920ef..b1f3a43a66d 100644
--- a/app/assets/javascripts/design_management_new/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management_legacy/components/design_notes/design_note.vue
@@ -60,7 +60,7 @@ export default {
},
mounted() {
if (this.isNoteLinked) {
- this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
+ this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
}
},
methods: {
@@ -80,7 +80,7 @@ export default {
</script>
<template>
- <timeline-entry-item :id="`note_${noteAnchorId}`" class="design-note note-form">
+ <timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form">
<user-avatar-link
:link-href="author.webUrl"
:img-src="author.avatarUrl"
diff --git a/app/assets/javascripts/design_management_new/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue
index 969034909f2..969034909f2 100644
--- a/app/assets/javascripts/design_management_new/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management_legacy/components/design_notes/design_reply_form.vue
diff --git a/app/assets/javascripts/design_management_new/components/design_notes/toggle_replies_widget.vue b/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue
index 46c73e3eea8..2e366282de3 100644
--- a/app/assets/javascripts/design_management_new/components/design_notes/toggle_replies_widget.vue
+++ b/app/assets/javascripts/design_management_legacy/components/design_notes/toggle_replies_widget.vue
@@ -52,18 +52,18 @@ export default {
{{ toggleText }}
</gl-button>
<template v-if="collapsed">
- <span class="gl-text-gray-700">{{ __('Last reply by') }}</span>
+ <span class="gl-text-gray-500">{{ __('Last reply by') }}</span>
<gl-link
:href="lastReply.author.webUrl"
target="_blank"
- class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2"
+ class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-ml-2 gl-mr-2"
>
{{ lastReply.author.name }}
</gl-link>
<time-ago-tooltip
:time="lastReply.createdAt"
tooltip-placement="bottom"
- class="gl-text-gray-700"
+ class="gl-text-gray-500"
/>
</template>
</li>
diff --git a/app/assets/javascripts/design_management_new/components/design_overlay.vue b/app/assets/javascripts/design_management_legacy/components/design_overlay.vue
index 926e7c74802..926e7c74802 100644
--- a/app/assets/javascripts/design_management_new/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management_legacy/components/design_overlay.vue
diff --git a/app/assets/javascripts/design_management_new/components/design_presentation.vue b/app/assets/javascripts/design_management_legacy/components/design_presentation.vue
index 84dbb2809d9..84dbb2809d9 100644
--- a/app/assets/javascripts/design_management_new/components/design_presentation.vue
+++ b/app/assets/javascripts/design_management_legacy/components/design_presentation.vue
diff --git a/app/assets/javascripts/design_management_new/components/design_scaler.vue b/app/assets/javascripts/design_management_legacy/components/design_scaler.vue
index 55dee74bef5..55dee74bef5 100644
--- a/app/assets/javascripts/design_management_new/components/design_scaler.vue
+++ b/app/assets/javascripts/design_management_legacy/components/design_scaler.vue
diff --git a/app/assets/javascripts/design_management_new/components/design_sidebar.vue b/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue
index 333ad2557e8..622120e2008 100644
--- a/app/assets/javascripts/design_management_new/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management_legacy/components/design_sidebar.vue
@@ -1,8 +1,8 @@
<script>
-import { s__ } from '~/locale';
import Cookies from 'js-cookie';
-import { parseBoolean } from '~/lib/utils/common_utils';
import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { parseBoolean } from '~/lib/utils/common_utils';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import { extractDiscussions, extractParticipants } from '../utils/design_management_utils';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
@@ -94,7 +94,7 @@ export default {
{{ issue.title }}
</h2>
<a
- class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
+ class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
:href="issue.webUrl"
>{{ issue.webPath }}</a
>
@@ -132,7 +132,7 @@ export default {
data-testid="resolved-comments"
:icon="resolvedCommentsToggleIcon"
variant="link"
- class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-mb-4"
+ class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
@click="$emit('toggleResolvedComments')"
>{{ $options.resolveCommentsToggleText }} ({{ resolvedDiscussions.length }})
</gl-button>
diff --git a/app/assets/javascripts/design_management_new/components/image.vue b/app/assets/javascripts/design_management_legacy/components/image.vue
index 91b7b576e0c..91b7b576e0c 100644
--- a/app/assets/javascripts/design_management_new/components/image.vue
+++ b/app/assets/javascripts/design_management_legacy/components/image.vue
diff --git a/app/assets/javascripts/design_management_new/components/list/item.vue b/app/assets/javascripts/design_management_legacy/components/list/item.vue
index b19aef9c22d..13c703b8a88 100644
--- a/app/assets/javascripts/design_management_new/components/list/item.vue
+++ b/app/assets/javascripts/design_management_legacy/components/list/item.vue
@@ -127,10 +127,10 @@ export default {
params: { id: filename },
query: $route.query,
}"
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
>
<div class="card-body p-0 d-flex-center overflow-hidden position-relative">
- <div v-if="icon.name" class="design-event position-absolute">
+ <div v-if="icon.name" data-testid="designEvent" class="design-event position-absolute">
<span :title="icon.tooltip" :aria-label="icon.tooltip">
<icon :name="icon.name" :size="18" :class="icon.classes" />
</span>
diff --git a/app/assets/javascripts/design_management_new/components/toolbar/index.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue
index 0b51035e83e..b998dfc47b8 100644
--- a/app/assets/javascripts/design_management_new/components/toolbar/index.vue
+++ b/app/assets/javascripts/design_management_legacy/components/toolbar/index.vue
@@ -6,6 +6,7 @@ import timeagoMixin from '~/vue_shared/mixins/timeago';
import Pagination from './pagination.vue';
import DeleteButton from '../delete_button.vue';
import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
+import appDataQuery from '../../graphql/queries/app_data.query.graphql';
import { DESIGNS_ROUTE_NAME } from '../../router/constants';
export default {
@@ -54,17 +55,19 @@ export default {
permissions: {
createDesign: false,
},
+ projectPath: '',
+ issueIid: null,
};
},
- inject: {
- projectPath: {
- default: '',
- },
- issueIid: {
- default: '',
- },
- },
apollo: {
+ appData: {
+ query: appDataQuery,
+ manual: true,
+ result({ data: { projectPath, issueIid } }) {
+ this.projectPath = projectPath;
+ this.issueIid = issueIid;
+ },
+ },
permissions: {
query: permissionsQuery,
variables() {
@@ -99,7 +102,6 @@ export default {
query: $route.query,
}"
:aria-label="s__('DesignManagement|Go back to designs')"
- data-testid="close-design"
class="mr-3 text-plain d-flex justify-content-center align-items-center"
>
<icon :size="18" name="close" />
diff --git a/app/assets/javascripts/design_management/components/toolbar/pagination.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue
index bf62a8f66a6..bf62a8f66a6 100644
--- a/app/assets/javascripts/design_management/components/toolbar/pagination.vue
+++ b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination.vue
diff --git a/app/assets/javascripts/design_management/components/toolbar/pagination_button.vue b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue
index f00ecefca01..f00ecefca01 100644
--- a/app/assets/javascripts/design_management/components/toolbar/pagination_button.vue
+++ b/app/assets/javascripts/design_management_legacy/components/toolbar/pagination_button.vue
diff --git a/app/assets/javascripts/design_management_new/components/upload/button.vue b/app/assets/javascripts/design_management_legacy/components/upload/button.vue
index de8a38334ac..68555104a3c 100644
--- a/app/assets/javascripts/design_management_new/components/upload/button.vue
+++ b/app/assets/javascripts/design_management_legacy/components/upload/button.vue
@@ -1,10 +1,10 @@
<script>
-import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
export default {
components: {
- GlButton,
+ GlDeprecatedButton,
GlLoadingIcon,
},
directives: {
@@ -30,7 +30,7 @@ export default {
<template>
<div>
- <gl-button
+ <gl-deprecated-button
v-gl-tooltip.hover
:title="
s__(
@@ -39,12 +39,11 @@ export default {
"
:disabled="isSaving"
variant="success"
- size="small"
@click="openFileUpload"
>
{{ s__('DesignManagement|Upload designs') }}
<gl-loading-icon v-if="isSaving" inline class="ml-1" />
- </gl-button>
+ </gl-deprecated-button>
<input
ref="fileUpload"
diff --git a/app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue b/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue
index 7b829d63330..e435c84c959 100644
--- a/app/assets/javascripts/design_management_new/components/upload/design_dropzone.vue
+++ b/app/assets/javascripts/design_management_legacy/components/upload/design_dropzone.vue
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import uploadDesignMutation from '../../graphql/mutations/upload_design.mutation.graphql';
import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR } from '../../utils/error_messages';
import { isValidDesignFile } from '../../utils/design_management_utils';
@@ -12,12 +12,6 @@ export default {
GlLink,
GlSprintf,
},
- props: {
- hasDesigns: {
- type: Boolean,
- required: true,
- },
- },
data() {
return {
dragCounter: 0,
@@ -82,21 +76,25 @@ export default {
>
<slot>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
@click="openFileUpload"
>
- <div
- :class="{ 'gl-flex-direction-column': hasDesigns }"
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center"
- data-testid="dropzone-area"
- >
- <gl-icon name="upload" :size="24" :class="hasDesigns ? 'gl-mb-2' : 'gl-mr-4'" />
- <p class="gl-font-weight-bold gl-mb-0">
- <gl-sprintf :message="__('Drop or %{linkStart}upload%{linkEnd} Designs to attach')">
+ <div class="d-flex-center flex-column text-center">
+ <gl-icon name="doc-new" :size="48" class="mb-4" />
+ <p>
+ <gl-sprintf
+ :message="
+ __(
+ '%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}.',
+ )
+ "
+ >
+ <template #lineOne="{ content }"
+ ><span class="d-block">{{ content }}</span>
+ </template>
+
<template #link="{ content }">
- <gl-link class="gl-font-weight-normal" @click.stop="openFileUpload">
- {{ content }}
- </gl-link>
+ <gl-link class="h-100 w-100" @click.stop="openFileUpload">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
@@ -119,7 +117,7 @@ export default {
class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
>
<div v-show="!isDragDataValid" class="mw-50 text-center">
- <h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Oh no!') }}</h3>
+ <h3>{{ __('Oh no!') }}</h3>
<span>{{
__(
'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.',
@@ -127,7 +125,7 @@ export default {
}}</span>
</div>
<div v-show="isDragDataValid" class="mw-50 text-center">
- <h3 :class="{ 'gl-font-base gl-display-inline': !hasDesigns }">{{ __('Incoming!') }}</h3>
+ <h3>{{ __('Incoming!') }}</h3>
<span>{{ __('Drop your designs to start your upload.') }}</span>
</div>
</div>
diff --git a/app/assets/javascripts/design_management_new/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue
index 5299d0ce09e..879d2523848 100644
--- a/app/assets/javascripts/design_management_new/components/upload/design_version_dropdown.vue
+++ b/app/assets/javascripts/design_management_legacy/components/upload/design_version_dropdown.vue
@@ -1,13 +1,13 @@
<script>
-import { GlNewDropdown, GlNewDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import allVersionsMixin from '../../mixins/all_versions';
import { findVersionId } from '../../utils/design_management_utils';
export default {
components: {
- GlNewDropdown,
- GlNewDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
},
mixins: [allVersionsMixin],
computed: {
@@ -50,8 +50,8 @@ export default {
</script>
<template>
- <gl-new-dropdown :text="dropdownText" size="small" class="design-version-dropdown">
- <gl-new-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id">
+ <gl-deprecated-dropdown :text="dropdownText" variant="link" class="design-version-dropdown">
+ <gl-deprecated-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id">
<router-link
class="d-flex js-version-link"
:to="{ path: $route.path, query: { version: findVersionId(version.node.id) } }"
@@ -68,9 +68,9 @@ export default {
</div>
<i
v-if="findVersionId(version.node.id) === currentVersionId"
- class="fa fa-check pull-right"
+ class="fa fa-check float-right gl-mr-2"
></i>
</router-link>
- </gl-new-dropdown-item>
- </gl-new-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
</template>
diff --git a/app/assets/javascripts/design_management_new/constants.js b/app/assets/javascripts/design_management_legacy/constants.js
index 21ff361a277..21ff361a277 100644
--- a/app/assets/javascripts/design_management_new/constants.js
+++ b/app/assets/javascripts/design_management_legacy/constants.js
diff --git a/app/assets/javascripts/design_management_new/graphql.js b/app/assets/javascripts/design_management_legacy/graphql.js
index fae337aa75b..fae337aa75b 100644
--- a/app/assets/javascripts/design_management_new/graphql.js
+++ b/app/assets/javascripts/design_management_legacy/graphql.js
diff --git a/app/assets/javascripts/design_management_new/graphql/fragments/design.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql
index 4b1703e41c3..4b1703e41c3 100644
--- a/app/assets/javascripts/design_management_new/graphql/fragments/design.fragment.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/fragments/design.fragment.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/fragments/design_list.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql
index bc3132f9b42..bc3132f9b42 100644
--- a/app/assets/javascripts/design_management_new/graphql/fragments/design_list.fragment.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_list.fragment.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/fragments/design_note.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql
index 26edd2c0be1..26edd2c0be1 100644
--- a/app/assets/javascripts/design_management_new/graphql/fragments/design_note.fragment.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/fragments/design_note.fragment.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/fragments/diff_refs.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql
index 984a55814b0..984a55814b0 100644
--- a/app/assets/javascripts/design_management_new/graphql/fragments/diff_refs.fragment.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/fragments/diff_refs.fragment.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/fragments/discussion_resolved_status.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql
index 7483b508721..7483b508721 100644
--- a/app/assets/javascripts/design_management_new/graphql/fragments/discussion_resolved_status.fragment.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/fragments/discussion_resolved_status.fragment.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/fragments/note_permissions.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql
index c243e39f3d3..c243e39f3d3 100644
--- a/app/assets/javascripts/design_management_new/graphql/fragments/note_permissions.fragment.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/fragments/note_permissions.fragment.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/fragments/version.fragment.graphql b/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql
index 7eb40b12f51..7eb40b12f51 100644
--- a/app/assets/javascripts/design_management_new/graphql/fragments/version.fragment.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/fragments/version.fragment.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/mutations/create_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql
index c8ade328120..c8ade328120 100644
--- a/app/assets/javascripts/design_management_new/graphql/mutations/create_image_diff_note.mutation.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/mutations/create_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql
index 184ee6955dc..184ee6955dc 100644
--- a/app/assets/javascripts/design_management_new/graphql/mutations/create_note.mutation.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/mutations/create_note.mutation.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/mutations/destroy_design.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql
index 0b3cf636cdb..0b3cf636cdb 100644
--- a/app/assets/javascripts/design_management_new/graphql/mutations/destroy_design.mutation.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/mutations/destroy_design.mutation.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/mutations/toggle_resolve_discussion.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql
index 1157fc05d5f..1157fc05d5f 100644
--- a/app/assets/javascripts/design_management_new/graphql/mutations/toggle_resolve_discussion.mutation.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/mutations/update_active_discussion.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql
index a24b6737159..a24b6737159 100644
--- a/app/assets/javascripts/design_management_new/graphql/mutations/update_active_discussion.mutation.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/mutations/update_image_diff_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql
index 5562ca9d89f..5562ca9d89f 100644
--- a/app/assets/javascripts/design_management_new/graphql/mutations/update_image_diff_note.mutation.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_image_diff_note.mutation.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/mutations/update_note.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql
index b995e99fb6a..b995e99fb6a 100644
--- a/app/assets/javascripts/design_management_new/graphql/mutations/update_note.mutation.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/mutations/update_note.mutation.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql
index d694e6558a0..d694e6558a0 100644
--- a/app/assets/javascripts/design_management_new/graphql/mutations/upload_design.mutation.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/mutations/upload_design.mutation.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/queries/active_discussion.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql
index 111023cea68..111023cea68 100644
--- a/app/assets/javascripts/design_management_new/graphql/queries/active_discussion.query.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/queries/active_discussion.query.graphql
diff --git a/app/assets/javascripts/design_management/graphql/queries/app_data.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql
index e1269761206..e1269761206 100644
--- a/app/assets/javascripts/design_management/graphql/queries/app_data.query.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/queries/app_data.query.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/queries/design_permissions.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql
index a87b256dc95..a87b256dc95 100644
--- a/app/assets/javascripts/design_management_new/graphql/queries/design_permissions.query.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/queries/design_permissions.query.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/queries/get_design.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql
index 07a9af55787..07a9af55787 100644
--- a/app/assets/javascripts/design_management_new/graphql/queries/get_design.query.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design.query.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/queries/get_design_list.query.graphql b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql
index 121a50555b3..121a50555b3 100644
--- a/app/assets/javascripts/design_management_new/graphql/queries/get_design_list.query.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/queries/get_design_list.query.graphql
diff --git a/app/assets/javascripts/design_management_new/graphql/typedefs.graphql b/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql
index fdbad4a90e0..fdbad4a90e0 100644
--- a/app/assets/javascripts/design_management_new/graphql/typedefs.graphql
+++ b/app/assets/javascripts/design_management_legacy/graphql/typedefs.graphql
diff --git a/app/assets/javascripts/design_management_legacy/index.js b/app/assets/javascripts/design_management_legacy/index.js
new file mode 100644
index 00000000000..1fc5779515a
--- /dev/null
+++ b/app/assets/javascripts/design_management_legacy/index.js
@@ -0,0 +1,61 @@
+// This application is being moved, please do not touch this files
+// Please see https://gitlab.com/gitlab-org/gitlab/-/issues/14744#note_364468096 for details
+
+import $ from 'jquery';
+import Vue from 'vue';
+import createRouter from './router';
+import App from './components/app.vue';
+import apolloProvider from './graphql';
+import getDesignListQuery from './graphql/queries/get_design_list.query.graphql';
+import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
+
+export default () => {
+ const el = document.querySelector('.js-design-management');
+ const badge = document.querySelector('.js-designs-count');
+ const { issueIid, projectPath, issuePath } = el.dataset;
+ const router = createRouter(issuePath);
+
+ $('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => {
+ if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) {
+ router.push({ name: DESIGNS_ROUTE_NAME });
+ } else if (id === 'discussion') {
+ router.push({ name: ROOT_ROUTE_NAME });
+ }
+ });
+
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ projectPath,
+ issueIid,
+ activeDiscussion: {
+ __typename: 'ActiveDiscussion',
+ id: null,
+ source: null,
+ },
+ },
+ });
+
+ apolloProvider.clients.defaultClient
+ .watchQuery({
+ query: getDesignListQuery,
+ variables: {
+ fullPath: projectPath,
+ iid: issueIid,
+ atVersion: null,
+ },
+ })
+ .subscribe(({ data }) => {
+ if (badge) {
+ badge.textContent = data.project.issue.designCollection.designs.edges.length;
+ }
+ });
+
+ return new Vue({
+ el,
+ router,
+ apolloProvider,
+ render(createElement) {
+ return createElement(App);
+ },
+ });
+};
diff --git a/app/assets/javascripts/design_management_new/mixins/all_designs.js b/app/assets/javascripts/design_management_legacy/mixins/all_designs.js
index f7d6551c46c..544429928d2 100644
--- a/app/assets/javascripts/design_management_new/mixins/all_designs.js
+++ b/app/assets/javascripts/design_management_legacy/mixins/all_designs.js
@@ -1,5 +1,5 @@
import { propertyOf } from 'lodash';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
import { extractNodes } from '../utils/design_management_utils';
diff --git a/app/assets/javascripts/design_management_new/mixins/all_versions.js b/app/assets/javascripts/design_management_legacy/mixins/all_versions.js
index 99e2ee9561c..3966fe71732 100644
--- a/app/assets/javascripts/design_management_new/mixins/all_versions.js
+++ b/app/assets/javascripts/design_management_legacy/mixins/all_versions.js
@@ -1,8 +1,17 @@
import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
+import appDataQuery from '../graphql/queries/app_data.query.graphql';
import { findVersionId } from '../utils/design_management_utils';
export default {
apollo: {
+ appData: {
+ query: appDataQuery,
+ manual: true,
+ result({ data: { projectPath, issueIid } }) {
+ this.projectPath = projectPath;
+ this.issueIid = issueIid;
+ },
+ },
allVersions: {
query: getDesignListQuery,
variables() {
@@ -15,14 +24,6 @@ export default {
update: data => data.project.issue.designCollection.versions.edges,
},
},
- inject: {
- projectPath: {
- default: '',
- },
- issueIid: {
- default: '',
- },
- },
computed: {
hasValidVersion() {
return (
@@ -54,6 +55,8 @@ export default {
data() {
return {
allVersions: [],
+ projectPath: '',
+ issueIid: null,
};
},
};
diff --git a/app/assets/javascripts/design_management_new/pages/design/index.vue b/app/assets/javascripts/design_management_legacy/pages/design/index.vue
index 47f5e3a786f..2ada9eff8c6 100644
--- a/app/assets/javascripts/design_management_new/pages/design/index.vue
+++ b/app/assets/javascripts/design_management_legacy/pages/design/index.vue
@@ -2,7 +2,7 @@
import Mousetrap from 'mousetrap';
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import allVersionsMixin from '../../mixins/all_versions';
import Toolbar from '../../components/toolbar/index.vue';
@@ -12,6 +12,7 @@ import DesignPresentation from '../../components/design_presentation.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
import DesignSidebar from '../../components/design_sidebar.vue';
import getDesignQuery from '../../graphql/queries/get_design.query.graphql';
+import appDataQuery from '../../graphql/queries/app_data.query.graphql';
import createImageDiffNoteMutation from '../../graphql/mutations/create_image_diff_note.mutation.graphql';
import updateImageDiffNoteMutation from '../../graphql/mutations/update_image_diff_note.mutation.graphql';
import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
@@ -61,12 +62,22 @@ export default {
design: {},
comment: '',
annotationCoordinates: null,
+ projectPath: '',
errorMessage: '',
+ issueIid: '',
scale: 1,
resolvedDiscussionsExpanded: false,
};
},
apollo: {
+ appData: {
+ query: appDataQuery,
+ manual: true,
+ result({ data: { projectPath, issueIid } }) {
+ this.projectPath = projectPath;
+ this.issueIid = issueIid;
+ },
+ },
design: {
query: getDesignQuery,
// We want to see cached design version if we have one, and fetch newer version on the background to update discussions
diff --git a/app/assets/javascripts/design_management_new/pages/index.vue b/app/assets/javascripts/design_management_legacy/pages/index.vue
index 700fa903a9c..66008a193ce 100644
--- a/app/assets/javascripts/design_management_new/pages/index.vue
+++ b/app/assets/javascripts/design_management_legacy/pages/index.vue
@@ -1,6 +1,6 @@
<script>
-import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { GlLoadingIcon, GlDeprecatedButton, GlAlert } from '@gitlab/ui';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
import UploadButton from '../components/upload/button.vue';
import DeleteButton from '../components/delete_button.vue';
@@ -33,7 +33,7 @@ export default {
components: {
GlLoadingIcon,
GlAlert,
- GlButton,
+ GlDeprecatedButton,
UploadButton,
Design,
DesignDestroyer,
@@ -96,14 +96,6 @@ export default {
? s__('DesignManagement|Deselect all')
: s__('DesignManagement|Select all');
},
- isDesignListEmpty() {
- return !this.isSaving && !this.hasDesigns;
- },
- designDropzoneWrapperClass() {
- return this.isDesignListEmpty
- ? 'col-12'
- : 'gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3';
- },
},
mounted() {
this.toggleOnPasteListener(this.$route.name);
@@ -246,55 +238,51 @@ export default {
this.onUploadDesign([newFile]);
}
},
- toggleOnPasteListener() {
- document.addEventListener('paste', this.onDesignPaste);
- },
- toggleOffPasteListener() {
- document.removeEventListener('paste', this.onDesignPaste);
+ toggleOnPasteListener(route) {
+ if (route === DESIGNS_ROUTE_NAME) {
+ document.addEventListener('paste', this.onDesignPaste);
+ } else {
+ document.removeEventListener('paste', this.onDesignPaste);
+ }
},
},
beforeRouteUpdate(to, from, next) {
+ this.toggleOnPasteListener(to.name);
this.selectedDesigns = [];
next();
},
+ beforeRouteLeave(to, from, next) {
+ this.toggleOnPasteListener(to.name);
+ next();
+ },
};
</script>
<template>
- <div
- data-testid="designs-root"
- class="gl-mt-5"
- :class="{ 'designs-root': !isDesignListEmpty }"
- @mouseenter="toggleOnPasteListener"
- @mouseleave="toggleOffPasteListener"
- >
+ <div>
<header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
- <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full">
- <div>
- <span class="gl-font-weight-bold gl-mr-3">{{ s__('DesignManagement|Designs') }}</span>
- <design-version-dropdown />
- </div>
- <div v-show="hasDesigns" class="qa-selector-toolbar gl-display-flex">
- <gl-button
+ <div class="d-flex justify-content-between align-items-center w-100">
+ <design-version-dropdown />
+ <div :class="['qa-selector-toolbar', { 'd-flex': hasDesigns, 'd-none': !hasDesigns }]">
+ <gl-deprecated-button
v-if="isLatestVersion"
variant="link"
- size="small"
- class="gl-mr-2 js-select-all"
+ class="mr-2 js-select-all"
@click="toggleDesignsSelection"
- >{{ selectAllButtonText }}
- </gl-button>
+ >{{ selectAllButtonText }}</gl-deprecated-button
+ >
<design-destroyer
#default="{ mutate, loading }"
:filenames="selectedDesigns"
+ :project-path="projectPath"
+ :iid="issueIid"
@done="onDesignDelete"
@error="onDesignDeleteError"
>
<delete-button
v-if="isLatestVersion"
:is-deleting="loading"
- button-variant="danger"
- button-class="gl-mr-4"
- button-size="small"
+ button-class="btn-danger btn-inverted mr-2"
:has-selected-designs="hasSelectedDesigns"
@deleteSelectedDesigns="mutate()"
>
@@ -312,22 +300,11 @@ export default {
{{ __('An error occurred while loading designs. Please try again.') }}
</gl-alert>
<ol v-else class="list-unstyled row">
- <span
- v-if="isDesignListEmpty && !allVersions.length"
- class="gl-font-weight-bold gl-font-weight-bold gl-ml-5 gl-mb-4"
- >{{ s__('DesignManagement|Designs') }}</span
- >
- <li :class="designDropzoneWrapperClass" data-testid="design-dropzone-wrapper">
- <design-dropzone
- :class="{ 'design-list-item design-list-item-new': !isDesignListEmpty }"
- :has-designs="hasDesigns"
- @change="onUploadDesign"
- />
+ <li class="col-md-6 col-lg-4 mb-3">
+ <design-dropzone class="design-list-item" @change="onUploadDesign" />
</li>
- <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-3 gl-mb-3">
- <design-dropzone
- :has-designs="hasDesigns"
- @change="onExistingDesignDropzoneChange($event, design.filename)"
+ <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3">
+ <design-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)"
><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)"
/></design-dropzone>
diff --git a/app/assets/javascripts/design_management_new/router/constants.js b/app/assets/javascripts/design_management_legacy/router/constants.js
index dd2ee8d8689..abeef520e33 100644
--- a/app/assets/javascripts/design_management_new/router/constants.js
+++ b/app/assets/javascripts/design_management_legacy/router/constants.js
@@ -1,2 +1,3 @@
+export const ROOT_ROUTE_NAME = 'root';
export const DESIGNS_ROUTE_NAME = 'designs';
export const DESIGN_ROUTE_NAME = 'design';
diff --git a/app/assets/javascripts/design_management_new/router/index.js b/app/assets/javascripts/design_management_legacy/router/index.js
index 40e2d35bc40..28a81ed0278 100644
--- a/app/assets/javascripts/design_management_new/router/index.js
+++ b/app/assets/javascripts/design_management_legacy/router/index.js
@@ -1,8 +1,9 @@
+import $ from 'jquery';
import Vue from 'vue';
import VueRouter from 'vue-router';
import routes from './routes';
import { DESIGN_ROUTE_NAME } from './constants';
-import { getPageLayoutElement } from '~/design_management_new/utils/design_management_utils';
+import { getPageLayoutElement } from '~/design_management_legacy/utils/design_management_utils';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '../constants';
Vue.use(VueRouter);
@@ -15,7 +16,9 @@ export default function createRouter(base) {
});
const pageEl = getPageLayoutElement();
- router.beforeEach(({ name }, _, next) => {
+ router.beforeEach(({ meta: { el }, name }, _, next) => {
+ $(`#${el}`).tab('show');
+
// apply a fullscreen layout style in Design View (a.k.a design detail)
if (pageEl) {
if (name === DESIGN_ROUTE_NAME) {
diff --git a/app/assets/javascripts/design_management_legacy/router/routes.js b/app/assets/javascripts/design_management_legacy/router/routes.js
new file mode 100644
index 00000000000..788910e5514
--- /dev/null
+++ b/app/assets/javascripts/design_management_legacy/router/routes.js
@@ -0,0 +1,44 @@
+import Home from '../pages/index.vue';
+import DesignDetail from '../pages/design/index.vue';
+import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
+
+export default [
+ {
+ name: ROOT_ROUTE_NAME,
+ path: '/',
+ component: Home,
+ meta: {
+ el: 'discussion',
+ },
+ },
+ {
+ name: DESIGNS_ROUTE_NAME,
+ path: '/designs',
+ component: Home,
+ meta: {
+ el: 'designs',
+ },
+ children: [
+ {
+ name: DESIGN_ROUTE_NAME,
+ path: ':id',
+ component: DesignDetail,
+ meta: {
+ el: 'designs',
+ },
+ beforeEnter(
+ {
+ params: { id },
+ },
+ from,
+ next,
+ ) {
+ if (typeof id === 'string') {
+ next();
+ }
+ },
+ props: ({ params: { id } }) => ({ id }),
+ },
+ ],
+ },
+];
diff --git a/app/assets/javascripts/design_management_new/utils/cache_update.js b/app/assets/javascripts/design_management_legacy/utils/cache_update.js
index 24b374b79fd..5ba6f84c413 100644
--- a/app/assets/javascripts/design_management_new/utils/cache_update.js
+++ b/app/assets/javascripts/design_management_legacy/utils/cache_update.js
@@ -1,6 +1,6 @@
/* eslint-disable @gitlab/require-i18n-strings */
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { extractCurrentDiscussion, extractDesign } from './design_management_utils';
import {
ADD_IMAGE_DIFF_NOTE_ERROR,
diff --git a/app/assets/javascripts/design_management_new/utils/design_management_utils.js b/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js
index 22705cf67a1..22705cf67a1 100644
--- a/app/assets/javascripts/design_management_new/utils/design_management_utils.js
+++ b/app/assets/javascripts/design_management_legacy/utils/design_management_utils.js
diff --git a/app/assets/javascripts/design_management_new/utils/error_messages.js b/app/assets/javascripts/design_management_legacy/utils/error_messages.js
index 7666c726c2f..7666c726c2f 100644
--- a/app/assets/javascripts/design_management_new/utils/error_messages.js
+++ b/app/assets/javascripts/design_management_legacy/utils/error_messages.js
diff --git a/app/assets/javascripts/design_management_new/utils/tracking.js b/app/assets/javascripts/design_management_legacy/utils/tracking.js
index b3ecc1453a6..b3ecc1453a6 100644
--- a/app/assets/javascripts/design_management_new/utils/tracking.js
+++ b/app/assets/javascripts/design_management_legacy/utils/tracking.js
diff --git a/app/assets/javascripts/design_management_new/components/toolbar/pagination_button.vue b/app/assets/javascripts/design_management_new/components/toolbar/pagination_button.vue
deleted file mode 100644
index f00ecefca01..00000000000
--- a/app/assets/javascripts/design_management_new/components/toolbar/pagination_button.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<script>
-import Icon from '~/vue_shared/components/icon.vue';
-import { DESIGN_ROUTE_NAME } from '../../router/constants';
-
-export default {
- components: {
- Icon,
- },
- props: {
- design: {
- type: Object,
- required: false,
- default: null,
- },
- title: {
- type: String,
- required: true,
- },
- iconName: {
- type: String,
- required: true,
- },
- },
- computed: {
- designLink() {
- if (!this.design) return {};
-
- return {
- name: DESIGN_ROUTE_NAME,
- params: { id: this.design.filename },
- query: this.$route.query,
- };
- },
- },
-};
-</script>
-
-<template>
- <router-link
- :to="designLink"
- :disabled="!design"
- :class="{ disabled: !design }"
- :aria-label="title"
- class="btn btn-default"
- >
- <icon :name="iconName" />
- </router-link>
-</template>
diff --git a/app/assets/javascripts/design_management_new/index.js b/app/assets/javascripts/design_management_new/index.js
deleted file mode 100644
index 20c9cacf83f..00000000000
--- a/app/assets/javascripts/design_management_new/index.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import Vue from 'vue';
-import createRouter from './router';
-import App from './components/app.vue';
-import apolloProvider from './graphql';
-
-export default () => {
- const el = document.querySelector('.js-design-management-new');
- const { issueIid, projectPath, issuePath } = el.dataset;
- const router = createRouter(issuePath);
-
- apolloProvider.clients.defaultClient.cache.writeData({
- data: {
- activeDiscussion: {
- __typename: 'ActiveDiscussion',
- id: null,
- source: null,
- },
- },
- });
-
- return new Vue({
- el,
- router,
- apolloProvider,
- provide: {
- projectPath,
- issueIid,
- },
- render(createElement) {
- return createElement(App);
- },
- });
-};
diff --git a/app/assets/javascripts/design_management_new/router/routes.js b/app/assets/javascripts/design_management_new/router/routes.js
deleted file mode 100644
index d888b856611..00000000000
--- a/app/assets/javascripts/design_management_new/router/routes.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import Home from '../pages/index.vue';
-import DesignDetail from '../pages/design/index.vue';
-import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
-
-export default [
- {
- name: DESIGNS_ROUTE_NAME,
- path: '/',
- component: Home,
- alias: '/designs',
- },
- {
- name: DESIGN_ROUTE_NAME,
- path: '/designs/:id',
- component: DesignDetail,
- beforeEnter(
- {
- params: { id },
- },
- _,
- next,
- ) {
- if (typeof id === 'string') {
- next();
- }
- },
- props: ({ params: { id } }) => ({ id }),
- },
-];
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index a343138a9e1..9497ea7bb4f 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import { getLocationHash } from './lib/utils/url_utility';
import FilesCommentButton from './files_comment_button';
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index 87e7dd18e0c..0943712d0c5 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import Vue from 'vue';
-import Flash from '../../flash';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import { sprintf, __ } from '~/locale';
const ResolveBtn = Vue.extend({
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index 27990b0a45e..d6975963977 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -1,7 +1,7 @@
/* global CommentsStore */
import Vue from 'vue';
-import Flash from '../../flash';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import { __ } from '~/locale';
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 1e524882d5f..5062006424e 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -1,9 +1,10 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlLoadingIcon, GlButtonGroup, GlButton } from '@gitlab/ui';
+import { GlLoadingIcon, GlButtonGroup, GlButton, GlAlert } from '@gitlab/ui';
import Mousetrap from 'mousetrap';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
@@ -38,6 +39,7 @@ export default {
PanelResizer,
GlButtonGroup,
GlButton,
+ GlAlert,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -127,7 +129,16 @@ export default {
emailPatchPath: state => state.diffs.emailPatchPath,
retrievingBatches: state => state.diffs.retrievingBatches,
}),
- ...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion', 'currentDiffFileId']),
+ ...mapState('diffs', [
+ 'showTreeList',
+ 'isLoading',
+ 'startVersion',
+ 'currentDiffFileId',
+ 'isTreeLoaded',
+ 'conflictResolutionPath',
+ 'canMerge',
+ 'hasConflicts',
+ ]),
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
@@ -155,6 +166,9 @@ export default {
isLimitedContainer() {
return !this.showTreeList && !this.isParallelView && !this.isFluidLayout;
},
+ isDiffHead() {
+ return parseBoolean(getParameterByName('diff_head'));
+ },
},
watch: {
commit(newCommit, oldCommit) {
@@ -400,12 +414,12 @@ export default {
<template>
<div v-show="shouldShow">
- <div v-if="isLoading" class="loading"><gl-loading-icon size="lg" /></div>
+ <div v-if="isLoading || !isTreeLoaded" class="loading"><gl-loading-icon size="lg" /></div>
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions
:merge-request-diffs="mergeRequestDiffs"
:is-limited-container="isLimitedContainer"
- :diff-files-length="diffFilesLength"
+ :diff-files-count-text="numTotalFiles"
/>
<hidden-files-warning
@@ -417,6 +431,49 @@ export default {
/>
<div
+ v-if="isDiffHead && hasConflicts"
+ :class="{
+ [CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
+ }"
+ >
+ <gl-alert
+ :dismissible="false"
+ :title="__('There are merge conflicts')"
+ variant="warning"
+ class="w-100 mb-3"
+ >
+ <p class="mb-1">
+ {{ __('The comparison view may be inaccurate due to merge conflicts.') }}
+ </p>
+ <p class="mb-0">
+ {{
+ __(
+ 'Resolve these conflicts or ask someone with write access to this repository to merge it locally.',
+ )
+ }}
+ </p>
+ <template #actions>
+ <gl-button
+ v-if="conflictResolutionPath"
+ :href="conflictResolutionPath"
+ variant="info"
+ class="mr-3 gl-alert-action"
+ >
+ {{ __('Resolve conflicts') }}
+ </gl-button>
+ <gl-button
+ v-if="canMerge"
+ class="gl-alert-action"
+ data-toggle="modal"
+ data-target="#modal_merge_info"
+ >
+ {{ __('Merge locally') }}
+ </gl-button>
+ </template>
+ </gl-alert>
+ </div>
+
+ <div
:data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex"
>
@@ -441,7 +498,7 @@ export default {
[CENTERED_LIMITED_CONTAINER_CLASSES]: isLimitedContainer,
}"
>
- <commit-widget v-if="commit" :commit="commit" />
+ <commit-widget v-if="commit" :commit="commit" :collapsible="false" />
<div v-if="isBatchLoading" class="loading"><gl-loading-icon size="lg" /></div>
<template v-else-if="renderDiffFiles">
<diff-file
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 99bc1b5c040..274a4027e62 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -52,10 +52,25 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
+ isSelectable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
commit: {
type: Object,
required: true,
},
+ checked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ collapsible: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
author() {
@@ -78,6 +93,10 @@ export default {
authorAvatar() {
return this.author.avatar_url || this.commit.author_gravatar_url;
},
+ commitDescription() {
+ // Strip the newline at the beginning
+ return this.commit.description_html.replace(/^&#x000A;/, '');
+ },
nextCommitUrl() {
return this.commit.next_commit_id
? setUrlParams({ commit_id: this.commit.next_commit_id })
@@ -104,14 +123,23 @@ export default {
</script>
<template>
- <li class="commit flex-row js-toggle-container">
- <user-avatar-link
- :link-href="authorUrl"
- :img-src="authorAvatar"
- :img-alt="authorName"
- :img-size="40"
- class="avatar-cell d-none d-sm-block"
- />
+ <li :class="{ 'js-toggle-container': collapsible }" class="commit flex-row">
+ <div class="d-flex align-items-center align-self-start">
+ <input
+ v-if="isSelectable"
+ class="mr-2"
+ type="checkbox"
+ :checked="checked"
+ @change="$emit('handleCheckboxChange', $event.target.checked)"
+ />
+ <user-avatar-link
+ :link-href="authorUrl"
+ :img-src="authorAvatar"
+ :img-alt="authorName"
+ :img-size="40"
+ class="avatar-cell d-none d-sm-block"
+ />
+ </div>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<a
@@ -123,7 +151,7 @@ export default {
<span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
<button
- v-if="commit.description_html"
+ v-if="commit.description_html && collapsible"
class="text-expander js-toggle-button"
type="button"
:aria-label="__('Toggle commit description')"
@@ -144,8 +172,9 @@ export default {
<pre
v-if="commit.description_html"
- class="commit-row-description js-toggle-content gl-mb-3"
- v-html="commit.description_html"
+ :class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
+ class="commit-row-description gl-mb-3 text-dark"
+ v-html="commitDescription"
></pre>
</div>
<div class="commit-actions flex-row d-none d-sm-flex">
diff --git a/app/assets/javascripts/diffs/components/commit_widget.vue b/app/assets/javascripts/diffs/components/commit_widget.vue
index 31ed003cc0f..5c7e84bd87c 100644
--- a/app/assets/javascripts/diffs/components/commit_widget.vue
+++ b/app/assets/javascripts/diffs/components/commit_widget.vue
@@ -23,15 +23,20 @@ export default {
type: Object,
required: true,
},
+ collapsible: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
<template>
- <div class="info-well w-100">
+ <div class="info-well mw-100 mx-0">
<div class="well-segment">
<ul class="blob-commit-info">
- <commit-item :commit="commit" />
+ <commit-item :commit="commit" :collapsible="collapsible" />
</ul>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 6f6fa312865..35e4527af69 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -32,9 +32,10 @@ export default {
required: false,
default: false,
},
- diffFilesLength: {
- type: Number,
- required: true,
+ diffFilesCountText: {
+ type: String,
+ required: false,
+ default: null,
},
},
computed: {
@@ -119,7 +120,7 @@ export default {
</div>
<div class="inline-parallel-buttons d-none d-md-flex ml-auto">
<diff-stats
- :diff-files-length="diffFilesLength"
+ :diff-files-count-text="diffFilesCountText"
:added-lines="addedLines"
:removed-lines="removedLines"
/>
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 741462a849c..087a558efdc 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -147,7 +147,7 @@ export default {
slot="image-overlay"
:discussions="imageDiscussions"
:file-hash="diffFileHash"
- :can-comment="getNoteableData.current_user.can_create_note"
+ :can-comment="getNoteableData.current_user.can_create_note && !diffFile.brokenSymlink"
/>
<div v-if="showNotesContainer" class="note-container">
<user-avatar-link
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 46ed76450c4..e5e63bdcb43 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapActions } from 'vuex';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import { UNFOLD_COUNT, INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 00d36c0b978..eace673c2d7 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -2,8 +2,9 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { escape } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, sprintf } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue';
@@ -16,6 +17,7 @@ export default {
DiffContent,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
file: {
type: Object,
@@ -89,8 +91,25 @@ export default {
this.setFileCollapsed({ filePath: this.file.file_path, collapsed: newVal });
},
+ 'file.file_hash': {
+ handler: function watchFileHash() {
+ if (
+ this.glFeatures.autoExpandCollapsedDiffs &&
+ this.viewDiffsFileByFile &&
+ this.file.viewer.collapsed
+ ) {
+ this.isCollapsed = false;
+ this.handleLoadCollapsedDiff();
+ } else {
+ this.isCollapsed = this.file.viewer.collapsed || false;
+ }
+ },
+ immediate: true,
+ },
'file.viewer.collapsed': function setIsCollapsed(newVal) {
- this.isCollapsed = newVal;
+ if (!this.viewDiffsFileByFile && !this.glFeatures.autoExpandCollapsedDiffs) {
+ this.isCollapsed = newVal;
+ }
},
},
created() {
@@ -148,6 +167,7 @@ export default {
:id="file.file_hash"
:class="{
'is-active': currentDiffFileId === file.file_hash,
+ 'comments-disabled': Boolean(file.brokenSymlink),
}"
:data-path="file.new_path"
class="diff-file file-holder"
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index d2f49bd0020..700e6302102 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -148,7 +148,7 @@ export default {
<template>
<div class="content discussion-form discussion-form-container discussion-notes">
- <div v-if="glFeatures.multilineComments" class="gl-mb-3 gl-text-gray-700 gl-pb-3">
+ <div v-if="glFeatures.multilineComments" class="gl-mb-3 gl-text-gray-500 gl-pb-3">
<multiline-comment-form
v-model="commentLineStart"
:line="line"
@@ -172,7 +172,7 @@ export default {
:diff-file="diffFile"
:show-suggest-popover="showSuggestPopover"
save-button-title="Comment"
- class="diff-comment-form prepend-top-10"
+ class="diff-comment-form gl-mt-3"
@handleFormUpdateAddToReview="addToReview"
@cancelForm="handleCancelCommentForm"
@handleFormUpdate="handleSaveNote"
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
index 0234fc4f40e..439d8097e56 100644
--- a/app/assets/javascripts/diffs/components/diff_stats.vue
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -1,7 +1,7 @@
<script>
+import { isNumber } from 'lodash';
import Icon from '~/vue_shared/components/icon.vue';
import { n__ } from '~/locale';
-import { isNumber } from 'lodash';
export default {
components: { Icon },
@@ -14,18 +14,21 @@ export default {
type: Number,
required: true,
},
- diffFilesLength: {
- type: Number,
+ diffFilesCountText: {
+ type: String,
required: false,
default: null,
},
},
computed: {
+ diffFilesLength() {
+ return parseInt(this.diffFilesCountText, 10);
+ },
filesText() {
return n__('file', 'files', this.diffFilesLength);
},
isCompareVersionsHeader() {
- return Boolean(this.diffFilesLength);
+ return Boolean(this.diffFilesCountText);
},
hasDiffFiles() {
return isNumber(this.diffFilesLength) && this.diffFilesLength >= 0;
@@ -44,7 +47,7 @@ export default {
>
<div v-if="hasDiffFiles" class="diff-stats-group">
<icon name="doc-code" class="diff-stats-icon text-secondary" />
- <span class="text-secondary bold">{{ diffFilesLength }} {{ filesText }}</span>
+ <span class="text-secondary bold">{{ diffFilesCountText }} {{ filesText }}</span>
</div>
<div
class="diff-stats-group cgreen d-flex align-items-center"
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index 198113e330a..49982a81372 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -1,8 +1,9 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
import DiffGutterAvatars from './diff_gutter_avatars.vue';
+import { __ } from '~/locale';
import {
CONTEXT_LINE_TYPE,
LINE_POSITION_RIGHT,
@@ -18,6 +19,9 @@ export default {
DiffGutterAvatars,
GlIcon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
line: {
type: Object,
@@ -123,6 +127,24 @@ export default {
lineNumber() {
return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line;
},
+ addCommentTooltip() {
+ const brokenSymlinks = this.line.commentsDisabled;
+ let tooltip = __('Add a comment to this line');
+
+ if (brokenSymlinks) {
+ if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
+ tooltip = __(
+ 'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
+ );
+ } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
+ tooltip = __(
+ 'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
+ );
+ }
+ }
+
+ return tooltip;
+ },
},
mounted() {
this.unwatchShouldShowCommentButton = this.$watch('shouldShowCommentButton', newVal => {
@@ -146,17 +168,24 @@ export default {
<template>
<td ref="td" :class="classNameMap">
- <button
- v-if="shouldRenderCommentButton"
- v-show="shouldShowCommentButton"
- ref="addDiffNoteButton"
- type="button"
- class="add-diff-note js-add-diff-note-button qa-diff-comment"
- title="Add a comment to this line"
- @click="handleCommentButton"
+ <span
+ ref="addNoteTooltip"
+ v-gl-tooltip
+ class="add-diff-note tooltip-wrapper"
+ :title="addCommentTooltip"
>
- <gl-icon :size="12" name="comment" />
- </button>
+ <button
+ v-if="shouldRenderCommentButton"
+ v-show="shouldShowCommentButton"
+ ref="addDiffNoteButton"
+ type="button"
+ class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ :disabled="line.commentsDisabled"
+ @click="handleCommentButton"
+ >
+ <gl-icon :size="12" name="comment" />
+ </button>
+ </span>
<a
v-if="lineNumber"
ref="lineNumberRef"
diff --git a/app/assets/javascripts/diffs/components/hidden_files_warning.vue b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
index ad0ca4fa402..baf7471582a 100644
--- a/app/assets/javascripts/diffs/components/hidden_files_warning.vue
+++ b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
@@ -26,7 +26,7 @@ export default {
<div class="alert alert-warning">
<h4>
{{ __('Too many changes to show.') }}
- <div class="pull-right">
+ <div class="float-right">
<a :href="plainDiffPath" class="btn btn-sm"> {{ __('Plain diff') }} </a>
<a :href="emailPatchPath" class="btn btn-sm"> {{ __('Email patch') }} </a>
</div>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index e3dd882f3dc..447136036ee 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -36,6 +36,9 @@ export const LENGTH_OF_AVATAR_TOOLTIP = 17;
export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
export const MAX_LINES_TO_BE_RENDERED = 2000;
+export const DIFF_FILE_SYMLINK_MODE = '120000';
+export const DIFF_FILE_DELETED_MODE = '0';
+
export const MR_TREE_SHOW_KEY = 'mr_tree_show';
export const TREE_TYPE = 'tree';
diff --git a/app/assets/javascripts/diffs/diff_file.js b/app/assets/javascripts/diffs/diff_file.js
new file mode 100644
index 00000000000..717c4a79ef9
--- /dev/null
+++ b/app/assets/javascripts/diffs/diff_file.js
@@ -0,0 +1,28 @@
+import { DIFF_FILE_SYMLINK_MODE, DIFF_FILE_DELETED_MODE } from './constants';
+
+function fileSymlinkInformation(file, fileList) {
+ const duplicates = fileList.filter(iteratedFile => iteratedFile.file_hash === file.file_hash);
+ const includesSymlink = duplicates.some(iteratedFile => {
+ return [iteratedFile.a_mode, iteratedFile.b_mode].includes(DIFF_FILE_SYMLINK_MODE);
+ });
+ const brokenSymlinkScenario = duplicates.length > 1 && includesSymlink;
+
+ return (
+ brokenSymlinkScenario && {
+ replaced: file.b_mode === DIFF_FILE_DELETED_MODE,
+ wasSymbolic: file.a_mode === DIFF_FILE_SYMLINK_MODE,
+ isSymbolic: file.b_mode === DIFF_FILE_SYMLINK_MODE,
+ wasReal: ![DIFF_FILE_SYMLINK_MODE, DIFF_FILE_DELETED_MODE].includes(file.a_mode),
+ isReal: ![DIFF_FILE_SYMLINK_MODE, DIFF_FILE_DELETED_MODE].includes(file.b_mode),
+ }
+ );
+}
+
+/* eslint-disable-next-line import/prefer-default-export */
+export function prepareRawDiffFile({ file, allFiles }) {
+ Object.assign(file, {
+ brokenSymlink: fileSymlinkInformation(file, allFiles),
+ });
+
+ return file;
+}
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 76ff67ab861..06a138b1e13 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
+import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import eventHub from '../notes/event_hub';
import diffsApp from './components/app.vue';
import { TREE_LIST_STORAGE_KEY, DIFF_WHITESPACE_COOKIE_NAME } from './constants';
-import Cookies from 'js-cookie';
export default function initDiffsApp(store) {
const fileFinderEl = document.getElementById('js-diff-file-finder');
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index fcc4a8160f4..d5581474c9b 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -3,7 +3,7 @@ import Cookies from 'js-cookie';
import Poll from '~/lib/utils/poll';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, s__ } from '~/locale';
import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
@@ -743,14 +743,14 @@ export function moveToNeighboringCommit({ dispatch, state }, { direction }) {
}
}
-export const setCurrentDiffFileIdFromNote = ({ commit, rootGetters }, noteId) => {
+export const setCurrentDiffFileIdFromNote = ({ commit, state, rootGetters }, noteId) => {
const note = rootGetters.notesById[noteId];
if (!note) return;
const fileHash = rootGetters.getDiscussion(note.discussion_id).diff_file?.file_hash;
- if (fileHash) {
+ if (fileHash && state.diffFiles.some(f => f.file_hash === fileHash)) {
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
}
};
@@ -761,6 +761,3 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash);
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 047caed1e63..a24894b8d6b 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -74,7 +74,6 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) =
discussion => discussion.diff_discussion && discussion.diff_file.file_hash === diff.file_hash,
) || [];
-// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests
export const getDiffFileByHash = state => fileHash =>
state.diffFiles.find(file => file.file_hash === fileHash);
@@ -130,6 +129,3 @@ export const fileLineCoverage = state => (file, line) => {
*/
export const currentDiffIndex = state =>
Math.max(0, state.diffFiles.findIndex(diff => diff.file_hash === state.currentDiffFileId));
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 1f165dd4971..d31a600e354 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -15,6 +15,7 @@ const whiteSpaceFromCookie = Cookies.get(DIFF_WHITESPACE_COOKIE_NAME);
export default () => ({
isLoading: true,
+ isTreeLoaded: false,
isBatchLoading: false,
retrievingBatches: false,
addedLines: null,
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 7e89d041c21..0d41f1c2178 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -323,6 +323,7 @@ export default {
[types.SET_TREE_DATA](state, { treeEntries, tree }) {
state.treeEntries = treeEntries;
state.tree = tree;
+ state.isTreeLoaded = true;
},
[types.SET_RENDER_TREE_LIST](state, renderTreeList) {
state.renderTreeList = renderTreeList;
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index bc85dd0a1d4..f014cddda32 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -18,6 +18,7 @@ import {
SHOW_WHITESPACE,
NO_SHOW_WHITESPACE,
} from '../constants';
+import { prepareRawDiffFile } from '../diff_file';
export function findDiffFile(files, match, matchKey = 'file_hash') {
return files.find(file => file[matchKey] === match);
@@ -294,9 +295,10 @@ function cleanRichText(text) {
return text ? text.replace(/^[+ -]/, '') : undefined;
}
-function prepareLine(line) {
+function prepareLine(line, file) {
if (!line.alreadyPrepared) {
Object.assign(line, {
+ commentsDisabled: file.brokenSymlink,
rich_text: cleanRichText(line.rich_text),
discussionsExpanded: true,
discussions: [],
@@ -330,7 +332,7 @@ export function prepareLineForRenamedFile({ line, diffViewType, diffFile, index
old_line: lineNumber,
};
- prepareLine(cleanLine); // WARNING: In-Place Mutations!
+ prepareLine(cleanLine, diffFile); // WARNING: In-Place Mutations!
if (diffViewType === PARALLEL_DIFF_VIEW_TYPE) {
return {
@@ -348,19 +350,19 @@ function prepareDiffFileLines(file) {
const parallelLines = file.parallel_diff_lines;
let parallelLinesCount = 0;
- inlineLines.forEach(prepareLine);
+ inlineLines.forEach(line => prepareLine(line, file)); // WARNING: In-Place Mutations!
parallelLines.forEach((line, index) => {
Object.assign(line, { line_code: getLineCode(line, index) });
if (line.left) {
parallelLinesCount += 1;
- prepareLine(line.left);
+ prepareLine(line.left, file); // WARNING: In-Place Mutations!
}
if (line.right) {
parallelLinesCount += 1;
- prepareLine(line.right);
+ prepareLine(line.right, file); // WARNING: In-Place Mutations!
}
});
@@ -407,6 +409,7 @@ function deduplicateFilesList(files) {
export function prepareDiffData(diff, priorFiles = []) {
const cleanedFiles = (diff.diff_files || [])
+ .map((file, index, allFiles) => prepareRawDiffFile({ file, allFiles }))
.map(ensureBasicDiffFileLines)
.map(prepareDiffFileLines)
.map(finalizeDiffFile);
@@ -477,6 +480,10 @@ export function getDiffPositionByLineCode(diffFiles, useSingleDiffStyle) {
// This method will check whether the discussion is still applicable
// to the diff line in question regarding different versions of the MR
export function isDiscussionApplicableToLine({ discussion, diffPosition, latestDiff }) {
+ if (!diffPosition) {
+ return false;
+ }
+
const { line_code, ...dp } = diffPosition;
// Removing `line_range` from diffPosition because the backend does not
// yet consistently return this property. This check can be removed,
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 9a0b85bd610..f65e22a31c5 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -70,7 +70,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
headers: csrf.headers,
previewContainer: false,
...config,
- processing: () => $('.div-dropzone-alert').alert('close'),
dragover: () => {
$mdArea.addClass('is-dropzone-hover');
form.find('.div-dropzone-hover').css('opacity', 0.7);
@@ -245,8 +244,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
$uploadingErrorMessage.html(message);
};
- const closeAlertMessage = () => form.find('.div-dropzone-alert').alert('close');
-
const insertToTextArea = (filename, url) => {
const $child = $(child);
const textarea = $child.get(0);
@@ -266,7 +263,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
formData.append('file', item, filename);
showSpinner();
- closeAlertMessage();
axios
.post(uploadsPath, formData)
diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js
index 551ffbabaef..0af0c3ecdcf 100644
--- a/app/assets/javascripts/editor/editor_lite.js
+++ b/app/assets/javascripts/editor/editor_lite.js
@@ -3,6 +3,7 @@ import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import languages from '~/ide/lib/languages';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import { registerLanguages } from '~/ide/utils';
+import { joinPaths } from '~/lib/utils/url_utility';
import { clearDomElement } from './utils';
export default class Editor {
@@ -30,7 +31,16 @@ export default class Editor {
monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
}
- createInstance({ el = undefined, blobPath = '', blobContent = '' } = {}) {
+ /**
+ * Creates a monaco instance with the given options.
+ *
+ * @param {Object} options Options used to initialize monaco.
+ * @param {Element} options.el The element which will be used to create the monacoEditor.
+ * @param {string} options.blobPath The path used as the URI of the model. Monaco uses the extension of this path to determine the language.
+ * @param {string} options.blobContent The content to initialize the monacoEditor.
+ * @param {string} options.blobGlobalId This is used to help globally identify monaco instances that are created with the same blobPath.
+ */
+ createInstance({ el = undefined, blobPath = '', blobContent = '', blobGlobalId = '' } = {}) {
if (!el) return;
this.editorEl = el;
this.blobContent = blobContent;
@@ -38,11 +48,9 @@ export default class Editor {
clearDomElement(this.editorEl);
- this.model = monacoEditor.createModel(
- this.blobContent,
- undefined,
- new Uri('gitlab', false, this.blobPath),
- );
+ const uriFilePath = joinPaths('gitlab', blobGlobalId, blobPath);
+
+ this.model = monacoEditor.createModel(this.blobContent, undefined, Uri.file(uriFilePath));
monacoEditor.onDidCreateEditor(this.renderEditor.bind(this));
@@ -51,6 +59,11 @@ export default class Editor {
}
dispose() {
+ if (this.model) {
+ this.model.dispose();
+ this.model = null;
+ }
+
return this.instance && this.instance.dispose();
}
@@ -58,6 +71,10 @@ export default class Editor {
delete this.editorEl.dataset.editorLoading;
}
+ onChangeContent(fn) {
+ return this.model.onDidChangeContent(fn);
+ }
+
updateModelLanguage(path) {
if (path === this.blobPath) return;
this.blobPath = path;
diff --git a/app/assets/javascripts/environments/components/enable_review_app_button.vue b/app/assets/javascripts/environments/components/enable_review_app_button.vue
index f86072696c2..8fbbc5189bf 100644
--- a/app/assets/javascripts/environments/components/enable_review_app_button.vue
+++ b/app/assets/javascripts/environments/components/enable_review_app_button.vue
@@ -1,11 +1,11 @@
<script>
-import { GlDeprecatedButton, GlModal, GlModalDirective, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective, GlLink, GlSprintf } from '@gitlab/ui';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { s__ } from '~/locale';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlLink,
GlModal,
GlSprintf,
@@ -44,7 +44,7 @@ export default {
</script>
<template>
<div>
- <gl-deprecated-button
+ <gl-button
v-gl-modal="$options.modalInfo.id"
variant="info"
category="secondary"
@@ -52,7 +52,7 @@ export default {
class="js-enable-review-app-button"
>
{{ s__('Environments|Enable review app') }}
- </gl-deprecated-button>
+ </gl-button>
<gl-modal
:modal-id="$options.modalInfo.id"
:title="$options.modalInfo.title"
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index af537cfb991..793f7bf0681 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,6 +1,5 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
/**
@@ -8,7 +7,7 @@ import { s__ } from '~/locale';
*/
export default {
components: {
- Icon,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -27,15 +26,14 @@ export default {
};
</script>
<template>
- <a
+ <gl-button
v-gl-tooltip
:title="title"
:aria-label="title"
:href="externalUrl"
- class="btn external-url"
+ class="external-url"
target="_blank"
+ icon="external-link"
rel="noopener noreferrer nofollow"
- >
- <icon name="external-link" />
- </a>
+ />
</template>
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index 99f50b499d0..c63d54d586d 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -5,16 +5,13 @@
*/
import $ from 'jquery';
-import { GlTooltipDirective } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
-import LoadingButton from '../../vue_shared/components/loading_button.vue';
export default {
components: {
- Icon,
- LoadingButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -55,16 +52,16 @@ export default {
};
</script>
<template>
- <loading-button
+ <gl-button
v-gl-tooltip
:loading="isLoading"
:title="title"
:aria-label="title"
- container-class="btn btn-danger d-none d-sm-none d-md-block"
+ icon="stop"
+ category="primary"
+ variant="danger"
data-toggle="modal"
data-target="#stop-environment-modal"
@click="onClick"
- >
- <icon name="stop" />
- </loading-button>
+ />
</template>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index d26bd14a937..f0e74d96f09 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,6 +1,6 @@
<script>
import { GlDeprecatedButton } from '@gitlab/ui';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { s__ } from '~/locale';
import emptyState from './empty_state.vue';
import eventHub from '../event_hub';
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index ab1818e61fa..1bf705dcda2 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -202,7 +202,7 @@ export default {
/>
<div :key="`sub-div-${i}`">
- <div class="text-center prepend-top-10">
+ <div class="text-center gl-mt-3">
<a :href="folderUrl(model)" class="btn btn-default">
{{ s__('Environments|Show all') }}
</a>
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index f2b464464e9..9b0301bba07 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -7,7 +7,7 @@ import EnvironmentsStore from 'ee_else_ce/environments/stores/environments_store
import Poll from '../../lib/utils/poll';
import { getParameterByName } from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
-import Flash from '../../flash';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import eventHub from '../event_hub';
import EnvironmentsService from '../services/environments_service';
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 52444d2c493..3d1fdc4f168 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -1,6 +1,5 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import createFlash from '~/flash';
import {
GlButton,
GlFormInput,
@@ -9,10 +8,11 @@ import {
GlBadge,
GlAlert,
GlSprintf,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
+ GlDeprecatedDropdownDivider,
} from '@gitlab/ui';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, sprintf, n__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
@@ -43,9 +43,9 @@ export default {
GlBadge,
GlAlert,
GlSprintf,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
+ GlDeprecatedDropdownDivider,
TimeAgoTooltip,
},
directives: {
@@ -331,38 +331,38 @@ export default {
</gl-button>
</form>
</div>
- <gl-dropdown
+ <gl-deprecated-dropdown
text="Options"
class="error-details-options d-md-none"
right
:disabled="issueUpdateInProgress"
>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
data-qa-selector="update_ignore_status_button"
@click="onIgnoreStatusUpdate"
- >{{ ignoreBtnLabel }}</gl-dropdown-item
+ >{{ ignoreBtnLabel }}</gl-deprecated-dropdown-item
>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
data-qa-selector="update_resolve_status_button"
@click="onResolveStatusUpdate"
- >{{ resolveBtnLabel }}</gl-dropdown-item
+ >{{ resolveBtnLabel }}</gl-deprecated-dropdown-item
>
- <gl-dropdown-divider />
- <gl-dropdown-item
+ <gl-deprecated-dropdown-divider />
+ <gl-deprecated-dropdown-item
v-if="error.gitlabIssuePath"
data-qa-selector="view_issue_button"
:href="error.gitlabIssuePath"
variant="success"
- >{{ __('View issue') }}</gl-dropdown-item
+ >{{ __('View issue') }}</gl-deprecated-dropdown-item
>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-if="!error.gitlabIssuePath"
:loading="issueCreationInProgress"
data-qa-selector="create_issue_button"
@click="createIssue"
- >{{ __('Create issue') }}</gl-dropdown-item
+ >{{ __('Create issue') }}</gl-deprecated-dropdown-item
>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</div>
</div>
<div>
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue b/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
index 0e3fd70b17b..db61957d452 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_actions.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlIcon, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlIcon, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
const IGNORED = 'ignored';
@@ -10,7 +10,7 @@ const statusValidation = [IGNORED, RESOLVED, UNRESOLVED];
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlIcon,
GlButtonGroup,
},
@@ -44,37 +44,37 @@ export default {
<template>
<div>
- <gl-button-group class="flex-column flex-md-row ml-0 ml-md-n4">
- <gl-deprecated-button
+ <gl-button-group class="gl-flex-direction-column flex-md-row gl-ml-0 ml-md-n4">
+ <gl-button
:key="ignoreBtn.status"
:ref="`${ignoreBtn.title.toLowerCase()}Error`"
v-gl-tooltip.hover
- class="d-block mb-2 mb-md-0 w-100"
+ class="gl-display-block gl-mb-4 mb-md-0 gl-w-full"
:title="ignoreBtn.title"
@click="$emit('update-issue-status', { errorId: error.id, status: ignoreBtn.status })"
>
- <gl-icon class="d-none d-md-inline m-0" :name="ignoreBtn.icon" :size="12" />
+ <gl-icon class="gl-display-none d-md-inline gl-m-0" :name="ignoreBtn.icon" :size="12" />
<span class="d-md-none">{{ ignoreBtn.title }}</span>
- </gl-deprecated-button>
- <gl-deprecated-button
+ </gl-button>
+ <gl-button
:key="resolveBtn.status"
:ref="`${resolveBtn.title.toLowerCase()}Error`"
v-gl-tooltip.hover
- class="d-block mb-2 mb-md-0 w-100"
+ class="gl-display-block gl-mb-4 mb-md-0 gl-w-full"
:title="resolveBtn.title"
@click="$emit('update-issue-status', { errorId: error.id, status: resolveBtn.status })"
>
- <gl-icon class="d-none d-md-inline m-0" :name="resolveBtn.icon" :size="12" />
+ <gl-icon class="gl-display-none d-md-inline gl-m-0" :name="resolveBtn.icon" :size="12" />
<span class="d-md-none">{{ resolveBtn.title }}</span>
- </gl-deprecated-button>
+ </gl-button>
</gl-button-group>
- <gl-deprecated-button
+ <gl-button
:href="detailsLink"
- category="secondary"
+ category="primary"
variant="info"
- class="d-block d-md-none mb-2 mb-md-0"
+ class="gl-display-block d-md-none gl-mb-4 mb-md-0"
>
{{ __('More details') }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 62a73e21096..da41dc4c9d9 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -2,22 +2,22 @@
import { mapActions, mapState } from 'vuex';
import {
GlEmptyState,
- GlDeprecatedButton,
+ GlButton,
GlIcon,
GlLink,
GlLoadingIcon,
GlTable,
GlFormInput,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
+ GlDeprecatedDropdownDivider,
GlTooltipDirective,
GlPagination,
} from '@gitlab/ui';
+import { isEmpty } from 'lodash';
import AccessorUtils from '~/lib/utils/accessor';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { __ } from '~/locale';
-import { isEmpty } from 'lodash';
import ErrorTrackingActions from './error_tracking_actions.vue';
import Tracking from '~/tracking';
import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '../utils';
@@ -71,10 +71,10 @@ export default {
},
components: {
GlEmptyState,
- GlDeprecatedButton,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
+ GlButton,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
+ GlDeprecatedDropdownDivider,
GlIcon,
GlLink,
GlLoadingIcon,
@@ -233,7 +233,7 @@ export default {
>
<div class="search-box flex-fill mb-1 mb-md-0">
<div class="filtered-search-box mb-0">
- <gl-dropdown
+ <gl-deprecated-dropdown
:text="__('Recent searches')"
class="filtered-search-history-dropdown-wrapper"
toggle-class="filtered-search-history-dropdown-toggle-button"
@@ -243,19 +243,19 @@ export default {
{{ __('This feature requires local storage to be enabled') }}
</div>
<template v-else-if="recentSearches.length > 0">
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="searchQuery in recentSearches"
:key="searchQuery"
@click="setSearchText(searchQuery)"
>{{ searchQuery }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches"
+ </gl-deprecated-dropdown-item>
+ <gl-deprecated-dropdown-divider />
+ <gl-deprecated-dropdown-item ref="clearRecentSearches" @click="clearRecentSearches"
>{{ __('Clear recent searches') }}
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
</template>
<div v-else class="px-3">{{ __("You don't have any recent searches") }}</div>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
<div class="filtered-search-input-container flex-fill">
<gl-form-input
v-model="errorSearchQuery"
@@ -267,27 +267,26 @@ export default {
/>
</div>
<div class="gl-search-box-by-type-right-icons">
- <gl-deprecated-button
+ <gl-button
v-if="errorSearchQuery.length > 0"
v-gl-tooltip.hover
:title="__('Clear')"
class="clear-search text-secondary"
name="clear"
+ icon="close"
@click="errorSearchQuery = ''"
- >
- <gl-icon name="close" :size="12" />
- </gl-deprecated-button>
+ />
</div>
</div>
</div>
- <gl-dropdown
+ <gl-deprecated-dropdown
:text="$options.statusFilters[statusFilter]"
class="status-dropdown mx-md-1 mb-1 mb-md-0"
menu-class="dropdown"
:disabled="loading"
>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="(label, status) in $options.statusFilters"
:key="status"
@click="filterErrors(status, label)"
@@ -300,16 +299,16 @@ export default {
/>
{{ label }}
</span>
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
- <gl-dropdown
+ <gl-deprecated-dropdown
:text="$options.sortFields[sortField]"
left
:disabled="loading"
menu-class="dropdown"
>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="(label, field) in $options.sortFields"
:key="field"
@click="sortByField(field)"
@@ -322,8 +321,8 @@ export default {
/>
{{ label }}
</span>
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
</div>
<div v-if="loading" class="py-3">
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index c22f34b5a8d..c6825d7af45 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -99,7 +99,7 @@ export default {
<gl-sprintf v-if="errorFn" :message="__('%{spanStart}in%{spanEnd} %{errorFn}')">
<template #span="{content}">
- <span class="gl-text-gray-400">{{ content }}&nbsp;</span>
+ <span class="gl-text-gray-200">{{ content }}&nbsp;</span>
</template>
<template #errorFn>
<strong>{{ errorFn }}&nbsp;</strong>
@@ -108,7 +108,7 @@ export default {
<gl-sprintf :message="__('%{spanStart}at line%{spanEnd} %{errorLine}%{errorColumn}')">
<template #span="{content}">
- <span class="gl-text-gray-400">{{ content }}&nbsp;</span>
+ <span class="gl-text-gray-200">{{ content }}&nbsp;</span>
</template>
<template #errorLine>
<strong>{{ errorLine }}</strong>
diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js
index 05554b2b566..b52405248d8 100644
--- a/app/assets/javascripts/error_tracking/store/actions.js
+++ b/app/assets/javascripts/error_tracking/store/actions.js
@@ -1,6 +1,6 @@
import service from '../services';
import * as types from './mutation_types';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -34,5 +34,3 @@ export const updateIgnoreStatus = ({ commit, dispatch }, params) => {
commit(types.SET_UPDATING_IGNORE_STATUS, false);
});
};
-
-export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js
index 5914a79f092..28806b3915c 100644
--- a/app/assets/javascripts/error_tracking/store/details/actions.js
+++ b/app/assets/javascripts/error_tracking/store/details/actions.js
@@ -1,6 +1,6 @@
import service from '../../services';
import * as types from './mutation_types';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
@@ -10,6 +10,7 @@ const stopPolling = poll => {
if (poll) poll.stop();
};
+// eslint-disable-next-line import/prefer-default-export
export function startPollingStacktrace({ commit }, endpoint) {
stackTracePoll = new Poll({
resource: service,
@@ -32,5 +33,3 @@ export function startPollingStacktrace({ commit }, endpoint) {
stackTracePoll.makeRequest();
}
-
-export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/details/getters.js b/app/assets/javascripts/error_tracking/store/details/getters.js
index a36c84dc28c..f2778fbb2c7 100644
--- a/app/assets/javascripts/error_tracking/store/details/getters.js
+++ b/app/assets/javascripts/error_tracking/store/details/getters.js
@@ -1,6 +1,5 @@
+// eslint-disable-next-line import/prefer-default-export
export const stacktrace = state =>
state.stacktraceData.stack_trace_entries
? state.stacktraceData.stack_trace_entries.reverse()
: [];
-
-export default () => {};
diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index 94cf444d2e4..a242c0e4236 100644
--- a/app/assets/javascripts/error_tracking/store/list/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/actions.js
@@ -1,6 +1,6 @@
import Service from '../../services';
import * as types from './mutation_types';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
@@ -102,5 +102,3 @@ export const fetchPaginatedResults = ({ commit, dispatch }, cursor) => {
export const removeIgnoredResolvedErrors = ({ commit }, error) => {
commit(types.REMOVE_IGNORED_RESOLVED_ERRORS, error);
};
-
-export default () => {};
diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue
index 7ae847e6105..db90ac1c740 100644
--- a/app/assets/javascripts/error_tracking_settings/components/app.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/app.vue
@@ -1,11 +1,11 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import ProjectDropdown from './project_dropdown.vue';
import ErrorTrackingForm from './error_tracking_form.vue';
export default {
- components: { ProjectDropdown, ErrorTrackingForm, GlDeprecatedButton },
+ components: { ProjectDropdown, ErrorTrackingForm, GlButton },
props: {
initialApiHost: {
type: String,
@@ -92,13 +92,15 @@ export default {
@select-project="updateSelectedProject"
/>
</div>
- <gl-deprecated-button
- :disabled="settingsLoading"
- class="js-error-tracking-button"
- variant="success"
- @click="handleSubmit"
- >
- {{ __('Save changes') }}
- </gl-deprecated-button>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button
+ :disabled="settingsLoading"
+ class="js-error-tracking-button"
+ variant="success"
+ @click="handleSubmit"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue b/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue
index 11fd06fb40b..561b2565880 100644
--- a/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue
@@ -1,11 +1,11 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import { getDisplayName } from '../utils';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
},
props: {
dropdownLabel: {
@@ -52,7 +52,7 @@ export default {
<div :class="{ 'gl-show-field-errors': isProjectInvalid }">
<label class="label-bold" for="project-dropdown">{{ __('Project') }}</label>
<div class="row">
- <gl-dropdown
+ <gl-deprecated-dropdown
id="project-dropdown"
class="col-8 col-md-9 gl-pr-0"
:disabled="!hasProjects"
@@ -60,14 +60,14 @@ export default {
toggle-class="dropdown-menu-toggle w-100 gl-field-error-outline"
:text="dropdownLabel"
>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="project in projects"
:key="`${project.organizationSlug}.${project.slug}`"
class="w-100"
@click="$emit('select-project', project)"
- >{{ getDisplayName(project) }}</gl-dropdown-item
+ >{{ getDisplayName(project) }}</gl-deprecated-dropdown-item
>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</div>
<p v-if="isProjectInvalid" class="js-project-dropdown-error gl-field-error">
{{ invalidProjectLabel }}
diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js
index 3f1ac426278..27433178c8e 100644
--- a/app/assets/javascripts/error_tracking_settings/store/actions.js
+++ b/app/assets/javascripts/error_tracking_settings/store/actions.js
@@ -1,7 +1,7 @@
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { transformFrontendSettings } from '../utils';
import * as types from './mutation_types';
@@ -89,6 +89,3 @@ export const updateSelectedProject = ({ commit }, selectedProject) => {
export const setInitialState = ({ commit }, data) => {
commit(types.SET_INITIAL_STATE, data);
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/error_tracking_settings/store/getters.js b/app/assets/javascripts/error_tracking_settings/store/getters.js
index e27fe9c079e..a02a4310ab9 100644
--- a/app/assets/javascripts/error_tracking_settings/store/getters.js
+++ b/app/assets/javascripts/error_tracking_settings/store/getters.js
@@ -39,6 +39,3 @@ export const projectSelectionLabel = state => {
}
return s__('ErrorTracking|To enable project selection, enter a valid Auth Token');
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/error_tracking_settings/utils.js b/app/assets/javascripts/error_tracking_settings/utils.js
index 450e8728121..9a09702a030 100644
--- a/app/assets/javascripts/error_tracking_settings/utils.js
+++ b/app/assets/javascripts/error_tracking_settings/utils.js
@@ -14,5 +14,3 @@ export const transformFrontendSettings = ({ apiHost, enabled, token, selectedPro
};
export const getDisplayName = project => `${project.organizationName} | ${project.slug}`;
-
-export default () => {};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
index fd9433b625c..cfadfb26db2 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
-import Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import LazyLoader from '../lazy_loader';
import { togglePopover } from '../shared/popover';
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 9440015b32e..80f78c154ee 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -1,21 +1,55 @@
import { __ } from '~/locale';
export default IssuableTokenKeys => {
- const wipToken = {
- formattedKey: __('WIP'),
- key: 'wip',
- type: 'string',
- param: '',
- symbol: '',
- icon: 'admin',
- tag: __('Yes or No'),
- lowercaseValueOnSubmit: true,
- uppercaseTokenName: true,
- capitalizeTokenValue: true,
+ const draftToken = {
+ token: {
+ formattedKey: __('Draft'),
+ key: 'draft',
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'admin',
+ tag: __('Yes or No'),
+ lowercaseValueOnSubmit: true,
+ capitalizeTokenValue: true,
+ },
+ conditions: [
+ {
+ url: 'wip=yes',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ replacementUrl: 'draft=yes',
+ tokenKey: 'draft',
+ value: __('Yes'),
+ operator: '=',
+ },
+ {
+ url: 'wip=no',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ replacementUrl: 'draft=no',
+ tokenKey: 'draft',
+ value: __('No'),
+ operator: '=',
+ },
+ {
+ url: 'not[wip]=yes',
+ replacementUrl: 'not[draft]=yes',
+ tokenKey: 'draft',
+ value: __('Yes'),
+ operator: '!=',
+ },
+ {
+ url: 'not[wip]=no',
+ replacementUrl: 'not[draft]=no',
+ tokenKey: 'draft',
+ value: __('No'),
+ operator: '!=',
+ },
+ ],
};
- IssuableTokenKeys.tokenKeys.push(wipToken);
- IssuableTokenKeys.tokenKeysWithAlternative.push(wipToken);
+ IssuableTokenKeys.tokenKeys.push(draftToken.token);
+ IssuableTokenKeys.tokenKeysWithAlternative.push(draftToken.token);
+ IssuableTokenKeys.conditions.push(...draftToken.conditions);
const targetBranchToken = {
formattedKey: __('Target-Branch'),
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 692b41da965..49bd3cda127 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -106,7 +106,7 @@ export default class AvailableDropdownMappings {
gl: DropdownEmoji,
element: this.container.querySelector('#js-dropdown-my-reaction'),
},
- wip: {
+ draft: {
reference: null,
gl: DropdownNonUser,
element: this.container.querySelector('#js-dropdown-wip'),
diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
index e2909333d74..0c4abc14494 100644
--- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
+++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
@@ -20,8 +20,18 @@ export default {
},
},
computed: {
+ /**
+ * Both Epic and Roadmap pages share same recents store
+ * and with https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36421
+ * Roadmap started using `GlFilteredSearch` which is not compatible
+ * with string tokens stored in recents, so this is a temporary
+ * fix by ignoring non-string recents while in Epic page.
+ */
+ compatibleItems() {
+ return this.items.filter(item => typeof item === 'string');
+ },
processedItems() {
- return this.items.map(item => {
+ return this.compatibleItems.map(item => {
const { tokens, searchToken } = FilteredSearchTokenizer.processTokens(
item,
this.allowedKeys,
@@ -41,7 +51,7 @@ export default {
});
},
hasItems() {
- return this.items.length > 0;
+ return this.compatibleItems.length > 0;
},
},
methods: {
@@ -84,9 +94,7 @@ export default {
<span class="value">{{ token.suffix }}</span>
</span>
</span>
- <span class="filtered-search-history-dropdown-search-token">
- {{ item.searchToken }}
- </span>
+ <span class="filtered-search-history-dropdown-search-token">{{ item.searchToken }}</span>
</button>
</li>
<li class="divider"></li>
diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
index 92a64ab60db..4652dfe71c3 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 { deprecatedCreateFlash as createFlash } from '../flash';
import AjaxFilter from '../droplab/plugins/ajax_filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
import DropdownUtils from './dropdown_utils';
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index adeea0ed5f6..1e3679b9e3c 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -1,4 +1,4 @@
-import Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index a2312de289d..bfa9f4a57ca 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 Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
import FilteredSearchDropdown from './filtered_search_dropdown';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 108cc8d3a78..3e4a9880134 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -3,7 +3,7 @@ import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searche
import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { visitUrl } from '../lib/utils/url_utility';
-import Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import FilteredSearchContainer from './container';
import RecentSearchesRoot from './recent_searches_root';
import RecentSearchesStore from './stores/recent_searches_store';
@@ -29,6 +29,7 @@ export default class FilteredSearchManager {
isGroup = false,
isGroupAncestor = true,
isGroupDecendent = false,
+ useDefaultState = false,
filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
placeholder = __('Search or filter results...'),
@@ -37,6 +38,7 @@ export default class FilteredSearchManager {
this.isGroup = isGroup;
this.isGroupAncestor = isGroupAncestor;
this.isGroupDecendent = isGroupDecendent;
+ this.useDefaultState = useDefaultState;
this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page;
@@ -724,8 +726,13 @@ export default class FilteredSearchManager {
search(state = null) {
const paths = [];
const { tokens, searchToken } = this.getSearchTokens();
- const currentState = state || getParameterByName('state') || 'opened';
- paths.push(`state=${currentState}`);
+ let currentState = state || getParameterByName('state');
+ if (!currentState && this.useDefaultState) {
+ currentState = 'opened';
+ }
+ if (this.states.includes(currentState)) {
+ paths.push(`state=${currentState}`);
+ }
tokens.forEach(token => {
const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue(
@@ -743,7 +750,7 @@ export default class FilteredSearchManager {
let tokenPath = '';
if (condition) {
- tokenPath = condition.url;
+ tokenPath = condition.replacementUrl || condition.url;
} else {
let tokenValue = token.value;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index 5298e20557d..f0951f6b177 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,5 +1,5 @@
import VisualTokenValue from './visual_token_value';
-import { objectToQueryString } from '~/lib/utils/common_utils';
+import { objectToQueryString, spriteIcon } from '~/lib/utils/common_utils';
import FilteredSearchContainer from './container';
export default class FilteredSearchVisualTokens {
@@ -84,7 +84,7 @@ export default class FilteredSearchVisualTokens {
<div class="value-container">
<div class="${capitalizeTokenValue ? 'text-capitalize' : ''} value"></div>
<div class="remove-token" role="button">
- <i class="fa fa-close"></i>
+ ${spriteIcon('close', 's16 close-icon')}
</div>
</div>
</div>
diff --git a/app/assets/javascripts/filtered_search/group_runners_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/group_runners_filtered_search_token_keys.js
new file mode 100644
index 00000000000..ceeb71c4eec
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/group_runners_filtered_search_token_keys.js
@@ -0,0 +1,27 @@
+import { __ } from '~/locale';
+import FilteredSearchTokenKeys from './filtered_search_token_keys';
+
+const tokenKeys = [
+ {
+ formattedKey: __('Status'),
+ key: 'status',
+ type: 'string',
+ param: 'status',
+ symbol: '',
+ icon: 'messages',
+ tag: 'status',
+ },
+ {
+ formattedKey: __('Type'),
+ key: 'type',
+ type: 'string',
+ param: 'type',
+ symbol: '',
+ icon: 'cube',
+ tag: 'type',
+ },
+];
+
+const GroupRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys);
+
+export default GroupRunnersFilteredSearchTokenKeys;
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index b1e6c4142e9..f73646da6d1 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 FilteredSearchContainer from '~/filtered_search/container';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
import AjaxCache from '~/lib/utils/ajax_cache';
import DropdownUtils from '~/filtered_search/dropdown_utils';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale';
import * as Emoji from '~/emoji';
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 74c00d21535..7697d97cb2c 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,3 +1,4 @@
+import * as Sentry from '@sentry/browser';
import { escape } from 'lodash';
import { spriteIcon } from './lib/utils/common_utils';
@@ -74,7 +75,7 @@ const removeFlashClickListener = (flashEl, fadeTransition) => {
* @param {Function} clickHandler Method to call when action is clicked on
* @param {Boolean} fadeTransition Boolean to determine whether to fade the alert out
*/
-const createFlash = function createFlash(
+const deprecatedCreateFlash = function deprecatedCreateFlash(
message,
type = FLASH_TYPES.ALERT,
parent = document,
@@ -109,12 +110,69 @@ const createFlash = function createFlash(
return flashContainer;
};
+/*
+ * Flash banner supports different types of Flash configurations
+ * along with ability to provide actionConfig which can be used to show
+ * additional action or link on banner next to message
+ *
+ * @param {Object} options Options to control the flash message
+ * @param {String} options.message Flash message text
+ * @param {String} options.type Type of Flash, it can be `notice`, `success`, `warning` or `alert` (default)
+ * @param {Object} options.parent Reference to parent element under which Flash needs to appear
+ * @param {Object} options.actonConfig Map of config to show action on banner
+ * @param {String} href URL to which action config should point to (default: '#')
+ * @param {String} title Title of action
+ * @param {Function} clickHandler Method to call when action is clicked on
+ * @param {Boolean} options.fadeTransition Boolean to determine whether to fade the alert out
+ * @param {Boolean} options.captureError Boolean to determine whether to send error to sentry
+ * @param {Object} options.error Error to be captured in sentry
+ */
+const createFlash = function createFlash({
+ message,
+ type = FLASH_TYPES.ALERT,
+ parent = document,
+ actionConfig = null,
+ fadeTransition = true,
+ addBodyClass = false,
+ captureError = false,
+ error = null,
+}) {
+ const flashContainer = parent.querySelector('.flash-container');
+
+ if (!flashContainer) return null;
+
+ flashContainer.innerHTML = createFlashEl(message, type);
+
+ const flashEl = flashContainer.querySelector(`.flash-${type}`);
+
+ if (actionConfig) {
+ flashEl.insertAdjacentHTML('beforeend', createAction(actionConfig));
+
+ if (actionConfig.clickHandler) {
+ flashEl
+ .querySelector('.flash-action')
+ .addEventListener('click', e => actionConfig.clickHandler(e));
+ }
+ }
+
+ removeFlashClickListener(flashEl, fadeTransition);
+
+ flashContainer.classList.add('gl-display-block');
+
+ if (addBodyClass) document.body.classList.add('flash-shown');
+
+ if (captureError && error) Sentry.captureException(error);
+
+ return flashContainer;
+};
+
export {
createFlash as default,
+ deprecatedCreateFlash,
createFlashEl,
createAction,
hideFlash,
removeFlashClickListener,
FLASH_TYPES,
};
-window.Flash = createFlash;
+window.Flash = deprecatedCreateFlash;
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
index ba62ab67e50..d4756e2ea6a 100644
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -76,6 +76,3 @@ export const setSearchQuery = ({ commit, dispatch }, query) => {
dispatch('fetchFrequentItems');
}
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/frequent_items/store/getters.js b/app/assets/javascripts/frequent_items/store/getters.js
index 00165db6684..73e66643f06 100644
--- a/app/assets/javascripts/frequent_items/store/getters.js
+++ b/app/assets/javascripts/frequent_items/store/getters.js
@@ -1,4 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
export const hasSearchQuery = state => state.searchQuery !== '';
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
index d4b7ffdcbe1..112e8eaaf17 100644
--- a/app/assets/javascripts/frequent_items/utils.js
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -1,6 +1,6 @@
import { take } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
@@ -52,7 +52,7 @@ export const sanitizeItem = item => {
return {};
}
- return { [key]: sanitize(item[key].toString(), { allowedTags: [] }) };
+ return { [key]: sanitize(item[key].toString(), { ALLOWED_TAGS: [] }) };
};
return {
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index 96051b612b5..099c46f4b8d 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
export default class GpgBadges {
diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
index 5295b2197d5..59327e36f5f 100644
--- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
+++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
@@ -1,11 +1,11 @@
<script>
-import { GlDeprecatedButton, GlFormGroup, GlFormInput, GlFormCheckbox } from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlFormCheckbox,
GlFormGroup,
GlFormInput,
@@ -56,9 +56,9 @@ export default {
<section id="grafana" class="settings no-animate js-grafana-integration">
<div class="settings-header">
<h3 class="js-section-header h4">
- {{ s__('GrafanaIntegration|Grafana Authentication') }}
+ {{ s__('GrafanaIntegration|Grafana authentication') }}
</h3>
- <gl-deprecated-button class="js-settings-toggle">{{ __('Expand') }}</gl-deprecated-button>
+ <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{ s__('GrafanaIntegration|Embed Grafana charts in GitLab issues.') }}
</p>
@@ -93,9 +93,11 @@ export default {
</a>
</p>
</gl-form-group>
- <gl-deprecated-button variant="success" @click="updateGrafanaIntegration">
- {{ __('Save Changes') }}
- </gl-deprecated-button>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button variant="success" category="primary" @click="updateGrafanaIntegration">
+ {{ __('Save Changes') }}
+ </gl-button>
+ </div>
</form>
</div>
</section>
diff --git a/app/assets/javascripts/grafana_integration/store/actions.js b/app/assets/javascripts/grafana_integration/store/actions.js
index d83f1e0831c..d28e59925d4 100644
--- a/app/assets/javascripts/grafana_integration/store/actions.js
+++ b/app/assets/javascripts/grafana_integration/store/actions.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import * as mutationTypes from './mutation_types';
diff --git a/app/assets/javascripts/graphql_shared/fragments/pageInfoCursorsOnly.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/pageInfoCursorsOnly.fragment.graphql
new file mode 100644
index 00000000000..22bcefbecd3
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/pageInfoCursorsOnly.fragment.graphql
@@ -0,0 +1,4 @@
+fragment PageInfo on PageInfo {
+ startCursor
+ endCursor
+}
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index ec8a238192a..a840e995860 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,6 +1,6 @@
import { slugify } from './lib/utils/text_utility';
import fetchGroupPathAvailability from '~/pages/groups/new/fetch_group_path_availability';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
export default class Group {
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index 9b74560f914..8c6c0714ee8 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
const tooltipTitles = {
group: __('Unsubscribe at group level'),
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index 985ea5a9019..ac4c12dda24 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -53,7 +53,7 @@ export default {
:aria-label="leaveBtnTitle"
data-container="body"
data-placement="bottom"
- class="leave-group btn btn-xs no-expand gl-text-gray-700 gl-ml-5"
+ class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
@click.prevent="onLeaveGroup"
>
<icon name="leave" class="position-top-0" />
@@ -66,7 +66,7 @@ export default {
:aria-label="editBtnTitle"
data-container="body"
data-placement="bottom"
- class="edit-group btn btn-xs no-expand gl-text-gray-700 gl-ml-5"
+ class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
>
<icon name="settings" class="position-top-0 align-middle" />
</a>
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index ffe4b18dea1..e09df8a5d26 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -1,5 +1,6 @@
<script>
import { GlBadge } from '@gitlab/ui';
+import isProjectPendingRemoval from 'ee_else_ce/groups/mixins/is_project_pending_removal';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
ITEM_TYPE,
@@ -8,7 +9,6 @@ import {
PROJECT_VISIBILITY_TYPE,
} from '../constants';
import itemStatsValue from './item_stats_value.vue';
-import isProjectPendingRemoval from 'ee_else_ce/groups/mixins/is_project_pending_removal';
export default {
components: {
@@ -44,7 +44,7 @@ export default {
</script>
<template>
- <div class="stats gl-text-gray-700">
+ <div class="stats gl-text-gray-500">
<item-stats-value
v-if="isGroup"
:title="__('Subgroups')"
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 4daa8c60e58..4c50bb3a9ac 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import { escape } from 'lodash';
import axios from './lib/utils/axios_utils';
import Api from './api';
-import { escape } from 'lodash';
import { normalizeHeaders } from './lib/utils/common_utils';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/helpers/monitor_helper.js b/app/assets/javascripts/helpers/monitor_helper.js
index 5f85ee58779..5e345321013 100644
--- a/app/assets/javascripts/helpers/monitor_helper.js
+++ b/app/assets/javascripts/helpers/monitor_helper.js
@@ -49,7 +49,7 @@ const multiMetricLabel = metricAttributes => {
* @param {Object} metricAttributes - Default metric attribute values (e.g. method, instance)
* @returns {String} The formatted query label
*/
-const getSeriesLabel = (queryLabel, metricAttributes) => {
+export const getSeriesLabel = (queryLabel, metricAttributes) => {
return (
singleAttributeLabel(queryLabel, metricAttributes) ||
templatedLabel(queryLabel, metricAttributes) ||
@@ -63,7 +63,6 @@ const getSeriesLabel = (queryLabel, metricAttributes) => {
* @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
* @returns {Array} The formatted values
*/
-// eslint-disable-next-line import/prefer-default-export
export const makeDataSeries = (queryResults, defaultConfig) =>
queryResults.map(result => {
return {
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
index 59a32dd477e..bbcb866c758 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions } from 'vuex';
-import { sprintf, __ } from '~/locale';
import { GlModal } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 3bba4fbc906..9342ab87c1a 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -1,7 +1,7 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import { n__, __ } from '~/locale';
import { GlModal } from '@gitlab/ui';
+import { n__, __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
@@ -138,7 +138,7 @@ export default {
@input="updateCommitMessage"
@submit="commit"
/>
- <div class="clearfix prepend-top-15">
+ <div class="clearfix gl-mt-5">
<actions />
<loading-button
:loading="submitCommitLoading"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 5cff1079eb0..d1422a506e7 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions } from 'vuex';
-import { __, sprintf } from '~/locale';
import { GlModal } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 03304337839..1b257ca11cc 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -84,7 +84,7 @@ export default {
:title="additionsTooltip"
data-container="body"
data-placement="left"
- class="append-bottom-10"
+ class="gl-mb-3"
>
<icon :name="additionIconName" :size="18" :class="addedFilesIconClass" />
</div>
@@ -94,7 +94,7 @@ export default {
:title="modifiedTooltip"
data-container="body"
data-placement="left"
- class="prepend-top-10 append-bottom-10"
+ class="gl-mt-3 gl-mb-3"
>
<icon :name="modifiedIconName" :size="18" :class="modifiedFilesClass" />
</div>
diff --git a/app/assets/javascripts/ide/components/ide_project_header.vue b/app/assets/javascripts/ide/components/ide_project_header.vue
index 36bc7c70196..36891505230 100644
--- a/app/assets/javascripts/ide/components/ide_project_header.vue
+++ b/app/assets/javascripts/ide/components/ide_project_header.vue
@@ -20,7 +20,11 @@ export default {
<project-avatar-default :project="project" :size="48" />
<span class="ide-sidebar-project-title">
<span class="sidebar-context-title"> {{ project.name }} </span>
- <span class="sidebar-context-title text-secondary">
+ <span
+ class="sidebar-context-title text-secondary"
+ data-qa-selector="project_path_content"
+ :data-qa-project-path="project.path_with_namespace"
+ >
{{ project.path_with_namespace }}
</span>
</span>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index fe0167942b8..44986c8c575 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
-import flash from '~/flash';
-import { __, sprintf, s__ } from '~/locale';
import { GlModal } from '@gitlab/ui';
+import { deprecatedCreateFlash as flash } from '~/flash';
+import { __, sprintf, s__ } from '~/locale';
import { modalTypes } from '../../constants';
import { trimPathComponents, getPathParent } from '../../utils';
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index ac445a1d9f1..d22d430cb4a 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,7 +1,7 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import {
@@ -167,7 +167,7 @@ export default {
},
mounted() {
if (!this.editor) {
- this.editor = Editor.create(this.editorOptions);
+ this.editor = Editor.create(this.$store, this.editorOptions);
}
this.initEditor();
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 152f77effa3..82cf8d7a10a 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import IdeRouter from '~/ide/ide_router_extension';
import { joinPaths } from '~/lib/utils/url_utility';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import { syncRouterAndStore } from './sync_router_and_store';
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 850cfcb05e3..7c767009de5 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import { mapActions } from 'vuex';
-import Translate from '~/vue_shared/translate';
import { identity } from 'lodash';
+import Translate from '~/vue_shared/translate';
import ide from './components/ide.vue';
-import store from './stores';
+import { createStore } from './stores';
import { createRouter } from './ide_router';
import { parseBoolean } from '../lib/utils/common_utils';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
@@ -32,6 +32,7 @@ export function initIde(el, options = {}) {
if (!el) return null;
const { rootComponent = ide, extendStore = identity } = options;
+ const store = createStore();
const router = createRouter(store);
return new Vue({
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 6e90968f008..f061fcb1259 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -1,6 +1,5 @@
import { debounce } from 'lodash';
import { editor as monacoEditor, KeyCode, KeyMod, Range } from 'monaco-editor';
-import store from '../stores';
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
@@ -20,14 +19,14 @@ function setupThemes() {
}
export default class Editor {
- static create(options = {}) {
+ static create(...args) {
if (!this.editorInstance) {
- this.editorInstance = new Editor(options);
+ this.editorInstance = new Editor(...args);
}
return this.editorInstance;
}
- constructor(options = {}) {
+ constructor(store, options = {}) {
this.currentModel = null;
this.instance = null;
this.dirtyDiffController = null;
@@ -42,6 +41,7 @@ export default class Editor {
...defaultDiffEditorOptions,
...options,
};
+ this.store = store;
setupThemes();
registerLanguages(...languages);
@@ -215,6 +215,7 @@ export default class Editor {
}
addCommands() {
+ const { store } = this;
const getKeyCode = key => {
const monacoKeyMod = key.indexOf('KEY_') === 0;
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index c881f1221e5..b083dc6325f 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index 3fdfdc5422b..547665b49c6 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 flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import service from '../../services';
import * as types from '../mutation_types';
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index d172bb31ae5..51e9bf6a84c 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 flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __, sprintf } from '~/locale';
import service from '../../services';
import api from '../../../api';
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index 18c466cc93d..324c5b0c6e4 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -33,5 +33,3 @@ export const createStoreOptions = () => ({
});
export const createStore = () => new Vuex.Store(createStoreOptions());
-
-export default createStore();
diff --git a/app/assets/javascripts/ide/stores/modules/branches/actions.js b/app/assets/javascripts/ide/stores/modules/branches/actions.js
index f90c2d77f2b..c46289f77e2 100644
--- a/app/assets/javascripts/ide/stores/modules/branches/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/branches/actions.js
@@ -32,5 +32,3 @@ export const fetchBranches = ({ dispatch, rootGetters }, { search = '' }) => {
};
export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES);
-
-export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 005bd0240e2..277e6923f17 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,5 +1,5 @@
import { sprintf, __ } from '~/locale';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index 453df8d7e0c..4a407aea557 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -18,10 +18,12 @@ export const templateTypes = () => [
name: __('Dockerfile'),
key: 'dockerfiles',
},
+ {
+ name: '.metrics-dashboard.yml',
+ key: 'metrics_dashboard_ymls',
+ },
];
export const showFileTemplatesBar = (_, getters, rootState) => name =>
getters.templateTypes.find(t => t.name === name) &&
rootState.currentActivityView === leftSidebarViews.edit.name;
-
-export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
index 8b5f7558654..6a1a0de033e 100644
--- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js
@@ -41,5 +41,3 @@ export const fetchMergeRequests = (
};
export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS);
-
-export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 9862c556c2e..86b889546b0 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -149,5 +149,3 @@ export const resetLatestPipeline = ({ commit }) => {
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null);
commit(types.SET_DETAIL_JOB, null);
};
-
-export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
index 1d127d915d7..eb3cc027494 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js
@@ -20,5 +20,3 @@ export const failedJobsCount = state =>
);
export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0);
-
-export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/index.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/index.js
index 112b3794114..5c13b5d74f2 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/index.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/index.js
@@ -2,4 +2,3 @@ export * from './setup';
export * from './checks';
export * from './session_controls';
export * from './session_status';
-export default () => {};
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 d3dcb9dd125..f20f7fc9cd6 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,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import * as types from '../mutation_types';
import * as messages from '../messages';
import * as terminalService from '../../../../services/terminals';
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 59ba1605c47..d715d555aa9 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,5 +1,5 @@
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import * as types from '../mutation_types';
import * as messages from '../messages';
import { isEndingStatus } from '../utils';
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/getters.js b/app/assets/javascripts/ide/stores/modules/terminal/getters.js
index 6d64ee4ab6e..ef98547ccc4 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/getters.js
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
export const allCheck = state => {
const checks = Object.values(state.checks);
@@ -15,5 +16,3 @@ export const allCheck = state => {
message,
};
};
-
-export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/messages.js b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
index 38c5a8a28d8..bf35ce0f0bc 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/messages.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
@@ -21,7 +21,7 @@ export const EMPTY_RUNNERS = __(
'Configure GitLab runners to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}',
);
export const ERROR_CONFIG = __(
- 'Configure a <code>.gitlab-webide.yml</code> file in the <code>.gitlab</code> directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}',
+ 'Configure a %{codeStart}.gitlab-webide.yml%{codeEnd} file in the %{codeStart}.gitlab%{codeEnd} directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}',
);
export const ERROR_PERMISSION = __(
'You do not have permission to run the Web Terminal. Please contact a project administrator.',
@@ -34,6 +34,8 @@ export const configCheckError = (status, helpUrl) => {
{
helpStart: `<a href="${escape(helpUrl)}" target="_blank">`,
helpEnd: '</a>',
+ codeStart: '<code>',
+ codeEnd: '</code>',
},
false,
);
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index 9ec7b2c06ce..58a6712c232 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -1,6 +1,6 @@
-import { SIDE_LEFT, SIDE_RIGHT } from './constants';
import { languages } from 'monaco-editor';
import { flatten } from 'lodash';
+import { SIDE_LEFT, SIDE_RIGHT } from './constants';
const toLowerCase = x => x.toLowerCase();
diff --git a/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue b/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue
index f673a0e42dc..bc8aa522596 100644
--- a/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue
+++ b/app/assets/javascripts/import_projects/components/bitbucket_status_table.vue
@@ -9,6 +9,7 @@ export default {
GlSprintf,
GlLink,
},
+ inheritAttrs: false,
props: {
providerTitle: {
type: String,
@@ -28,7 +29,7 @@ export default {
};
</script>
<template>
- <import-projects-table :provider-title="providerTitle">
+ <import-projects-table :provider-title="providerTitle" v-bind="$attrs">
<template #actions>
<slot name="actions"></slot>
</template>
diff --git a/app/assets/javascripts/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_projects/components/import_projects_table.vue
index 6a467fb8c6a..72fdaca7e24 100644
--- a/app/assets/javascripts/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_projects/components/import_projects_table.vue
@@ -3,10 +3,12 @@ import { throttle } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import ImportedProjectTableRow from './imported_project_table_row.vue';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
import IncompatibleRepoTableRow from './incompatible_repo_table_row.vue';
-import eventHub from '../event_hub';
+import PageQueryParamSync from './page_query_param_sync.vue';
+import { isProjectImportable } from '../utils';
const reposFetchThrottleDelay = 1000;
@@ -16,8 +18,10 @@ export default {
ImportedProjectTableRow,
ProviderRepoTableRow,
IncompatibleRepoTableRow,
+ PageQueryParamSync,
GlLoadingIcon,
GlButton,
+ PaginationLinks,
},
props: {
providerTitle: {
@@ -29,23 +33,37 @@ export default {
required: false,
default: true,
},
+ paginatable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
- ...mapState([
- 'importedProjects',
- 'providerRepos',
- 'incompatibleRepos',
- 'isLoadingRepos',
- 'filter',
- ]),
+ ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']),
...mapGetters([
+ 'isLoading',
'isImportingAnyRepo',
- 'hasProviderRepos',
- 'hasImportedProjects',
+ 'hasImportableRepos',
'hasIncompatibleRepos',
]),
+ availableNamespaces() {
+ const serializedNamespaces = this.namespaces.map(({ fullPath }) => ({
+ id: fullPath,
+ text: fullPath,
+ }));
+
+ return [
+ { text: __('Groups'), children: serializedNamespaces },
+ {
+ text: __('Users'),
+ children: [{ id: this.defaultTargetNamespace, text: this.defaultTargetNamespace }],
+ },
+ ];
+ },
+
importAllButtonText() {
return this.hasIncompatibleRepos
? __('Import all compatible repositories')
@@ -64,7 +82,8 @@ export default {
},
mounted() {
- return this.fetchRepos();
+ this.fetchNamespaces();
+ this.fetchRepos();
},
beforeDestroy() {
@@ -75,17 +94,14 @@ export default {
methods: {
...mapActions([
'fetchRepos',
- 'fetchReposFiltered',
- 'fetchJobs',
+ 'fetchNamespaces',
'stopJobsPolling',
'clearJobsEtagPoll',
'setFilter',
+ 'importAll',
+ 'setPage',
]),
- importAll() {
- eventHub.$emit('importAll');
- },
-
handleFilterInput({ target }) {
this.setFilter(target.value);
},
@@ -93,79 +109,90 @@ export default {
throttledFetchRepos: throttle(function fetch() {
this.fetchRepos();
}, reposFetchThrottleDelay),
+
+ isProjectImportable,
},
};
</script>
<template>
<div>
+ <page-query-param-sync :page="pageInfo.page" @popstate="setPage" />
+
<p class="light text-nowrap mt-2">
{{ s__('ImportProjects|Select the projects you want to import') }}
</p>
<template v-if="hasIncompatibleRepos">
- <slot name="incompatible-repos-warning"> </slot>
+ <slot name="incompatible-repos-warning"></slot>
</template>
- <div
- v-if="!isLoadingRepos"
- class="d-flex justify-content-between align-items-end flex-wrap mb-3"
- >
- <gl-button
- variant="success"
- :loading="isImportingAnyRepo"
- :disabled="!hasProviderRepos"
- type="button"
- @click="importAll"
- >
- {{ importAllButtonText }}
- </gl-button>
- <slot name="actions"></slot>
- <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
- <input
- :value="filter"
- data-qa-selector="githubish_import_filter_field"
- class="form-control"
- name="filter"
- :placeholder="__('Filter your projects by name')"
- autofocus
- size="40"
- @input="handleFilterInput($event)"
- @keyup.enter="throttledFetchRepos"
- />
- </form>
- </div>
<gl-loading-icon
- v-if="isLoadingRepos"
+ v-if="isLoading"
class="js-loading-button-icon import-projects-loading-icon"
size="md"
/>
- <div
- v-else-if="hasProviderRepos || hasImportedProjects || hasIncompatibleRepos"
- class="table-responsive"
- >
- <table class="table import-table">
- <thead>
- <th class="import-jobs-from-col">{{ fromHeaderText }}</th>
- <th class="import-jobs-to-col">{{ __('To GitLab') }}</th>
- <th class="import-jobs-status-col">{{ __('Status') }}</th>
- <th class="import-jobs-cta-col"></th>
- </thead>
- <tbody>
- <imported-project-table-row
- v-for="project in importedProjects"
- :key="project.id"
- :project="project"
- />
- <provider-repo-table-row v-for="repo in providerRepos" :key="repo.id" :repo="repo" />
- <incompatible-repo-table-row
- v-for="repo in incompatibleRepos"
- :key="repo.id"
- :repo="repo"
+ <template v-if="!isLoading">
+ <div class="d-flex justify-content-between align-items-end flex-wrap mb-3">
+ <gl-button
+ variant="success"
+ :loading="isImportingAnyRepo"
+ :disabled="!hasImportableRepos"
+ type="button"
+ @click="importAll"
+ >{{ importAllButtonText }}</gl-button
+ >
+ <slot name="actions"></slot>
+ <form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
+ <input
+ :value="filter"
+ data-qa-selector="githubish_import_filter_field"
+ class="form-control"
+ name="filter"
+ :placeholder="__('Filter your projects by name')"
+ autofocus
+ size="40"
+ @input="handleFilterInput($event)"
+ @keyup.enter="throttledFetchRepos"
/>
- </tbody>
- </table>
- </div>
- <div v-else class="text-center">
- <strong>{{ emptyStateText }}</strong>
- </div>
+ </form>
+ </div>
+ <div v-if="repositories.length" class="table-responsive">
+ <table class="table import-table">
+ <thead>
+ <th class="import-jobs-from-col">{{ fromHeaderText }}</th>
+ <th class="import-jobs-to-col">{{ __('To GitLab') }}</th>
+ <th class="import-jobs-status-col">{{ __('Status') }}</th>
+ <th class="import-jobs-cta-col"></th>
+ </thead>
+ <tbody>
+ <template v-for="repo in repositories">
+ <incompatible-repo-table-row
+ v-if="repo.importSource.incompatible"
+ :key="repo.importSource.id"
+ :repo="repo"
+ />
+ <provider-repo-table-row
+ v-else-if="isProjectImportable(repo)"
+ :key="repo.importSource.id"
+ :repo="repo"
+ :available-namespaces="availableNamespaces"
+ />
+ <imported-project-table-row v-else :key="repo.importSource.id" :project="repo" />
+ </template>
+ </tbody>
+ </table>
+ </div>
+ <div v-else class="text-center">
+ <strong>{{ emptyStateText }}</strong>
+ </div>
+ <pagination-links
+ v-if="paginatable"
+ align="center"
+ class="gl-mt-3"
+ :page-info="pageInfo"
+ :prev-page="pageInfo.page - 1"
+ :next-page="repositories.length && pageInfo.page + 1"
+ :change="setPage"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
index ab2bd87ee9f..50e735b4478 100644
--- a/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
+++ b/app/assets/javascripts/import_projects/components/imported_project_table_row.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import ImportStatus from './import_status.vue';
import { STATUSES } from '../constants';
@@ -6,6 +7,7 @@ export default {
name: 'ImportedProjectTableRow',
components: {
ImportStatus,
+ GlIcon,
},
props: {
project: {
@@ -16,7 +18,7 @@ export default {
computed: {
displayFullPath() {
- return this.project.fullPath.replace(/^\//, '');
+ return this.project.importedProject.fullPath.replace(/^\//, '');
},
isFinished() {
@@ -27,28 +29,30 @@ export default {
</script>
<template>
- <tr class="js-imported-project import-row">
+ <tr class="import-row">
<td>
<a
- :href="project.providerLink"
+ :href="project.importSource.providerLink"
rel="noreferrer noopener"
target="_blank"
- class="js-provider-link"
- >
- {{ project.importSource }}
+ data-testid="providerLink"
+ >{{ project.importSource.fullName }}
+ <gl-icon v-if="project.importSource.providerLink" name="external-link" />
</a>
</td>
- <td class="js-full-path">{{ displayFullPath }}</td>
- <td><import-status :status="project.importStatus" /></td>
+ <td data-testid="fullPath">{{ displayFullPath }}</td>
+ <td>
+ <import-status :status="project.importStatus" />
+ </td>
<td>
<a
v-if="isFinished"
- class="btn btn-default js-go-to-project"
- :href="project.fullPath"
+ class="btn btn-default"
+ data-testid="goToProject"
+ :href="project.importedProject.fullPath"
rel="noreferrer noopener"
target="_blank"
- >
- {{ __('Go to project') }}
+ >{{ __('Go to project') }}
</a>
</td>
</tr>
diff --git a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue b/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
index fa2fb439eac..3140585ccd7 100644
--- a/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
+++ b/app/assets/javascripts/import_projects/components/incompatible_repo_table_row.vue
@@ -1,9 +1,10 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlIcon, GlBadge } from '@gitlab/ui';
export default {
components: {
GlBadge,
+ GlIcon,
},
props: {
repo: {
@@ -17,8 +18,9 @@ export default {
<template>
<tr class="import-row">
<td>
- <a :href="repo.providerLink" rel="noreferrer noopener" target="_blank">
- {{ repo.fullName }}
+ <a :href="repo.importSource.providerLink" rel="noreferrer noopener" target="_blank"
+ >{{ repo.importSource.fullName }}
+ <gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</a>
</td>
<td></td>
diff --git a/app/assets/javascripts/import_projects/components/page_query_param_sync.vue b/app/assets/javascripts/import_projects/components/page_query_param_sync.vue
new file mode 100644
index 00000000000..5ba3d70f5d0
--- /dev/null
+++ b/app/assets/javascripts/import_projects/components/page_query_param_sync.vue
@@ -0,0 +1,39 @@
+<script>
+import { queryToObject, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
+
+export default {
+ props: {
+ page: {
+ type: Number,
+ required: true,
+ },
+ },
+
+ watch: {
+ page(newPage) {
+ updateHistory({
+ url: setUrlParams({
+ page: newPage === 1 ? null : newPage,
+ }),
+ });
+ },
+ },
+
+ created() {
+ window.addEventListener('popstate', this.updatePage);
+ },
+
+ beforeDestroy() {
+ window.removeEventListener('popstate', this.updatePage);
+ },
+
+ methods: {
+ updatePage() {
+ const page = parseInt(queryToObject(window.location.search).page, 10) || 1;
+ this.$emit('popstate', page);
+ },
+ },
+
+ render: () => null,
+};
+</script>
diff --git a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
index 63524d61146..d8cffc6a7d5 100644
--- a/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_projects/components/provider_repo_table_row.vue
@@ -1,9 +1,8 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import Select2Select from '~/vue_shared/components/select2_select.vue';
import { __ } from '~/locale';
-import eventHub from '../event_hub';
-import { STATUSES } from '../constants';
import ImportStatus from './import_status.vue';
export default {
@@ -11,25 +10,26 @@ export default {
components: {
Select2Select,
ImportStatus,
+ GlIcon,
},
props: {
repo: {
type: Object,
required: true,
},
- },
-
- data() {
- return {
- targetNamespace: this.$store.state.defaultTargetNamespace,
- newName: this.repo.sanitizedName,
- };
+ availableNamespaces: {
+ type: Array,
+ required: true,
+ },
},
computed: {
- ...mapState(['namespaces', 'reposBeingImported', 'ciCdOnly']),
+ ...mapState(['ciCdOnly']),
+ ...mapGetters(['getImportTarget']),
- ...mapGetters(['namespaceSelectOptions']),
+ importTarget() {
+ return this.getImportTarget(this.repo.importSource.id);
+ },
importButtonText() {
return this.ciCdOnly ? __('Connect') : __('Import');
@@ -37,37 +37,36 @@ export default {
select2Options() {
return {
- data: this.namespaceSelectOptions,
- containerCssClass:
- 'import-namespace-select js-namespace-select qa-project-namespace-select w-auto',
+ data: this.availableNamespaces,
+ containerCssClass: 'import-namespace-select qa-project-namespace-select w-auto',
};
},
- isLoadingImport() {
- return this.reposBeingImported.includes(this.repo.id);
+ targetNamespaceSelect: {
+ get() {
+ return this.importTarget.targetNamespace;
+ },
+ set(value) {
+ this.updateImportTarget({ targetNamespace: value });
+ },
},
- status() {
- return this.isLoadingImport ? STATUSES.SCHEDULING : STATUSES.NONE;
+ newNameInput: {
+ get() {
+ return this.importTarget.newName;
+ },
+ set(value) {
+ this.updateImportTarget({ newName: value });
+ },
},
},
- created() {
- eventHub.$on('importAll', this.importRepo);
- },
-
- beforeDestroy() {
- eventHub.$off('importAll', this.importRepo);
- },
-
methods: {
- ...mapActions(['fetchImport']),
-
- importRepo() {
- return this.fetchImport({
- newName: this.newName,
- targetNamespace: this.targetNamespace,
- repo: this.repo,
+ ...mapActions(['fetchImport', 'setImportTarget']),
+ updateImportTarget(changedValues) {
+ this.setImportTarget({
+ repoId: this.repo.importSource.id,
+ importTarget: { ...this.importTarget, ...changedValues },
});
},
},
@@ -75,35 +74,39 @@ export default {
</script>
<template>
- <tr class="qa-project-import-row js-provider-repo import-row">
+ <tr class="qa-project-import-row import-row">
<td>
<a
- :href="repo.providerLink"
+ :href="repo.importSource.providerLink"
rel="noreferrer noopener"
target="_blank"
- class="js-provider-link"
- >
- {{ repo.fullName }}
+ data-testid="providerLink"
+ >{{ repo.importSource.fullName }}
+ <gl-icon v-if="repo.importSource.providerLink" name="external-link" />
</a>
</td>
<td class="d-flex flex-wrap flex-lg-nowrap">
- <select2-select v-model="targetNamespace" :options="select2Options" />
- <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
- >/</span
- >
- <input
- v-model="newName"
- type="text"
- class="form-control import-project-name-input js-new-name qa-project-path-field"
- />
+ <template v-if="repo.target">{{ repo.target }}</template>
+ <template v-else>
+ <select2-select v-model="targetNamespaceSelect" :options="select2Options" />
+ <span class="px-2 import-slash-divider d-flex justify-content-center align-items-center"
+ >/</span
+ >
+ <input
+ v-model="newNameInput"
+ type="text"
+ class="form-control import-project-name-input qa-project-path-field"
+ />
+ </template>
+ </td>
+ <td>
+ <import-status :status="repo.importStatus" />
</td>
- <td><import-status :status="status" /></td>
<td>
<button
- v-if="!isLoadingImport"
type="button"
- class="qa-import-button js-import-button btn btn-default"
- @click="importRepo"
+ class="qa-import-button btn btn-default"
+ @click="fetchImport(repo.importSource.id)"
>
{{ importButtonText }}
</button>
diff --git a/app/assets/javascripts/import_projects/index.js b/app/assets/javascripts/import_projects/index.js
index 68ba04aa9dd..79fbd58e355 100644
--- a/app/assets/javascripts/import_projects/index.js
+++ b/app/assets/javascripts/import_projects/index.js
@@ -2,28 +2,44 @@ import Vue from 'vue';
import Translate from '../vue_shared/translate';
import ImportProjectsTable from './components/import_projects_table.vue';
import { parseBoolean } from '../lib/utils/common_utils';
+import { queryToObject } from '../lib/utils/url_utility';
import createStore from './store';
Vue.use(Translate);
export function initStoreFromElement(element) {
const {
- reposPath,
- provider,
+ ciCdOnly,
canSelectNamespace,
+ provider,
+
+ reposPath,
jobsPath,
importPath,
- ciCdOnly,
+ namespacesPath,
+ paginatable,
} = element.dataset;
+ const params = queryToObject(document.location.search);
+ const page = parseInt(params.page ?? 1, 10);
+
return createStore({
- reposPath,
- provider,
- jobsPath,
- importPath,
- defaultTargetNamespace: gon.current_username,
- ciCdOnly: parseBoolean(ciCdOnly),
- canSelectNamespace: parseBoolean(canSelectNamespace),
+ initialState: {
+ defaultTargetNamespace: gon.current_username,
+ ciCdOnly: parseBoolean(ciCdOnly),
+ canSelectNamespace: parseBoolean(canSelectNamespace),
+ provider,
+ pageInfo: {
+ page,
+ },
+ },
+ endpoints: {
+ reposPath,
+ jobsPath,
+ importPath,
+ namespacesPath,
+ },
+ hasPagination: parseBoolean(paginatable),
});
}
@@ -31,6 +47,7 @@ export function initPropsFromElement(element) {
return {
providerTitle: element.dataset.providerTitle,
filterable: parseBoolean(element.dataset.filterable),
+ paginatable: parseBoolean(element.dataset.paginatable),
};
}
diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js
index 8d8d33f5972..af410f411d8 100644
--- a/app/assets/javascripts/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_projects/store/actions.js
@@ -1,41 +1,86 @@
import Visibility from 'visibilityjs';
import * as types from './mutation_types';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { isProjectImportable } from '../utils';
+import {
+ convertObjectPropsToCamelCase,
+ normalizeHeaders,
+ parseIntPagination,
+} from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
-import { visitUrl } from '~/lib/utils/url_utility';
-import createFlash from '~/flash';
+import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
-import { jobsPathWithFilter, reposPathWithFilter } from './getters';
let eTagPoll;
const hasRedirectInError = e => e?.response?.data?.error?.redirect;
const redirectToUrlInError = e => visitUrl(e.response.data.error.redirect);
+const pathWithParams = ({ path, ...params }) => {
+ const filteredParams = Object.fromEntries(
+ Object.entries(params).filter(([, value]) => value !== ''),
+ );
+ const queryString = objectToQuery(filteredParams);
+ return queryString ? `${path}?${queryString}` : path;
+};
+
+const isRequired = () => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('param is required');
+};
-export const clearJobsEtagPoll = () => {
+const clearJobsEtagPoll = () => {
eTagPoll = null;
};
-export const stopJobsPolling = () => {
+
+const stopJobsPolling = () => {
if (eTagPoll) eTagPoll.stop();
};
-export const restartJobsPolling = () => {
+
+const restartJobsPolling = () => {
if (eTagPoll) eTagPoll.restart();
};
-export const setFilter = ({ commit }, filter) => commit(types.SET_FILTER, filter);
+const setFilter = ({ commit }, filter) => commit(types.SET_FILTER, filter);
+
+const setImportTarget = ({ commit }, { repoId, importTarget }) =>
+ commit(types.SET_IMPORT_TARGET, { repoId, importTarget });
+
+const importAll = ({ state, dispatch }) => {
+ return Promise.all(
+ state.repositories
+ .filter(isProjectImportable)
+ .map(r => dispatch('fetchImport', r.importSource.id)),
+ );
+};
-export const fetchRepos = ({ state, dispatch, commit }) => {
+const fetchReposFactory = ({ reposPath = isRequired(), hasPagination }) => ({
+ state,
+ dispatch,
+ commit,
+}) => {
dispatch('stopJobsPolling');
commit(types.REQUEST_REPOS);
- const { provider } = state;
+ const { provider, filter } = state;
return axios
- .get(reposPathWithFilter(state))
- .then(({ data }) =>
- commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
+ .get(
+ pathWithParams({
+ path: reposPath,
+ filter,
+ page: hasPagination ? state.pageInfo.page.toString() : '',
+ }),
)
+ .then(({ data, headers }) => {
+ const normalizedHeaders = normalizeHeaders(headers);
+
+ if ('X-PAGE' in normalizedHeaders) {
+ commit(types.SET_PAGE_INFO, parseIntPagination(normalizedHeaders));
+ }
+
+ commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true }));
+ })
.then(() => dispatch('fetchJobs'))
.catch(e => {
if (hasRedirectInError(e)) {
@@ -52,24 +97,26 @@ export const fetchRepos = ({ state, dispatch, commit }) => {
});
};
-export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo }) => {
- if (!state.reposBeingImported.includes(repo.id)) {
- commit(types.REQUEST_IMPORT, repo.id);
- }
+const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, getters }, repoId) => {
+ const { ciCdOnly } = state;
+ const importTarget = getters.getImportTarget(repoId);
+
+ commit(types.REQUEST_IMPORT, { repoId, importTarget });
+ const { newName, targetNamespace } = importTarget;
return axios
- .post(state.importPath, {
- ci_cd_only: state.ciCdOnly,
+ .post(importPath, {
+ repo_id: repoId,
+ ci_cd_only: ciCdOnly,
new_name: newName,
- repo_id: repo.id,
target_namespace: targetNamespace,
})
- .then(({ data }) =>
+ .then(({ data }) => {
commit(types.RECEIVE_IMPORT_SUCCESS, {
importedProject: convertObjectPropsToCamelCase(data, { deep: true }),
- repoId: repo.id,
- }),
- )
+ repoId,
+ });
+ })
.catch(e => {
const serverErrorMessage = e?.response?.data?.errors;
const flashMessage = serverErrorMessage
@@ -84,14 +131,11 @@ export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo
createFlash(flashMessage);
- commit(types.RECEIVE_IMPORT_ERROR, repo.id);
+ commit(types.RECEIVE_IMPORT_ERROR, repoId);
});
};
-export const receiveJobsSuccess = ({ commit }, updatedProjects) =>
- commit(types.RECEIVE_JOBS_SUCCESS, updatedProjects);
-
-export const fetchJobs = ({ state, commit, dispatch }) => {
+export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, dispatch }) => {
const { filter } = state;
if (eTagPoll) {
@@ -101,7 +145,7 @@ export const fetchJobs = ({ state, commit, dispatch }) => {
eTagPoll = new Poll({
resource: {
- fetchJobs: () => axios.get(jobsPathWithFilter(state)),
+ fetchJobs: () => axios.get(pathWithParams({ path: jobsPath, filter })),
},
method: 'fetchJobs',
successCallback: ({ data }) =>
@@ -129,5 +173,39 @@ export const fetchJobs = ({ state, commit, dispatch }) => {
});
};
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
+const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) => {
+ commit(types.REQUEST_NAMESPACES);
+ axios
+ .get(namespacesPath)
+ .then(({ data }) =>
+ commit(types.RECEIVE_NAMESPACES_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
+ )
+ .catch(() => {
+ createFlash(s__('ImportProjects|Requesting namespaces failed'));
+
+ commit(types.RECEIVE_NAMESPACES_ERROR);
+ });
+};
+
+const setPage = ({ state, commit, dispatch }, page) => {
+ if (page === state.pageInfo.page) {
+ return null;
+ }
+
+ commit(types.SET_PAGE, page);
+ return dispatch('fetchRepos');
+};
+
+export default ({ endpoints = isRequired(), hasPagination }) => ({
+ clearJobsEtagPoll,
+ stopJobsPolling,
+ restartJobsPolling,
+ setFilter,
+ setImportTarget,
+ importAll,
+ setPage,
+ fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath, hasPagination }),
+ fetchImport: fetchImportFactory(endpoints.importPath),
+ fetchJobs: fetchJobsFactory(endpoints.jobsPath),
+ fetchNamespaces: fetchNamespacesFactory(endpoints.namespacesPath),
+});
diff --git a/app/assets/javascripts/import_projects/store/getters.js b/app/assets/javascripts/import_projects/store/getters.js
index e6eb8f523de..7d529c94d7d 100644
--- a/app/assets/javascripts/import_projects/store/getters.js
+++ b/app/assets/javascripts/import_projects/store/getters.js
@@ -1,29 +1,27 @@
-import { __ } from '~/locale';
+import { STATUSES } from '../constants';
-export const namespaceSelectOptions = state => {
- const serializedNamespaces = state.namespaces.map(({ fullPath }) => ({
- id: fullPath,
- text: fullPath,
- }));
+export const isLoading = state => state.isLoadingRepos || state.isLoadingNamespaces;
- return [
- { text: __('Groups'), children: serializedNamespaces },
- {
- text: __('Users'),
- children: [{ id: state.defaultTargetNamespace, text: state.defaultTargetNamespace }],
- },
- ];
-};
+export const isImportingAnyRepo = state =>
+ state.repositories.some(repo =>
+ [STATUSES.SCHEDULING, STATUSES.SCHEDULED, STATUSES.STARTED].includes(repo.importStatus),
+ );
-export const isImportingAnyRepo = state => state.reposBeingImported.length > 0;
+export const hasIncompatibleRepos = state =>
+ state.repositories.some(repo => repo.importSource.incompatible);
-export const hasProviderRepos = state => state.providerRepos.length > 0;
+export const hasImportableRepos = state =>
+ state.repositories.some(repo => repo.importStatus === STATUSES.NONE);
-export const hasImportedProjects = state => state.importedProjects.length > 0;
+export const getImportTarget = state => repoId => {
+ if (state.customImportTargets[repoId]) {
+ return state.customImportTargets[repoId];
+ }
-export const hasIncompatibleRepos = state => state.incompatibleRepos.length > 0;
+ const repo = state.repositories.find(r => r.importSource.id === repoId);
-export const reposPathWithFilter = ({ reposPath, filter = '' }) =>
- filter ? `${reposPath}?filter=${filter}` : reposPath;
-export const jobsPathWithFilter = ({ jobsPath, filter = '' }) =>
- filter ? `${jobsPath}?filter=${filter}` : jobsPath;
+ return {
+ newName: repo.importSource.sanitizedName,
+ targetNamespace: state.defaultTargetNamespace,
+ };
+};
diff --git a/app/assets/javascripts/import_projects/store/index.js b/app/assets/javascripts/import_projects/store/index.js
index 29deb7868ba..7ba12f81eb9 100644
--- a/app/assets/javascripts/import_projects/store/index.js
+++ b/app/assets/javascripts/import_projects/store/index.js
@@ -1,18 +1,16 @@
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
-import * as actions from './actions';
+import actionsFactory from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
-export { state, actions, getters, mutations };
-
-export default initialState =>
+export default ({ initialState, endpoints, hasPagination }) =>
new Vuex.Store({
state: { ...state(), ...initialState },
- actions,
+ actions: actionsFactory({ endpoints, hasPagination }),
mutations,
getters,
});
diff --git a/app/assets/javascripts/import_projects/store/mutation_types.js b/app/assets/javascripts/import_projects/store/mutation_types.js
index a23b7eef986..6adf5e59cff 100644
--- a/app/assets/javascripts/import_projects/store/mutation_types.js
+++ b/app/assets/javascripts/import_projects/store/mutation_types.js
@@ -2,6 +2,10 @@ export const REQUEST_REPOS = 'REQUEST_REPOS';
export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS';
export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR';
+export const REQUEST_NAMESPACES = 'REQUEST_NAMESPACES';
+export const RECEIVE_NAMESPACES_SUCCESS = 'RECEIVE_NAMESPACES_SUCCESS';
+export const RECEIVE_NAMESPACES_ERROR = 'RECEIVE_NAMESPACES_ERROR';
+
export const REQUEST_IMPORT = 'REQUEST_IMPORT';
export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS';
export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
@@ -9,3 +13,9 @@ export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
export const SET_FILTER = 'SET_FILTER';
+
+export const SET_IMPORT_TARGET = 'SET_IMPORT_TARGET';
+
+export const SET_PAGE = 'SET_PAGE';
+
+export const SET_PAGE_INFO = 'SET_PAGE_INFO';
diff --git a/app/assets/javascripts/import_projects/store/mutations.js b/app/assets/javascripts/import_projects/store/mutations.js
index ec62d0640ef..b3dbef896a6 100644
--- a/app/assets/javascripts/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_projects/store/mutations.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import * as types from './mutation_types';
+import { STATUSES } from '../constants';
export default {
[types.SET_FILTER](state, filter) {
@@ -12,48 +13,103 @@ export default {
[types.RECEIVE_REPOS_SUCCESS](
state,
- { importedProjects, providerRepos, incompatibleRepos, namespaces },
+ { importedProjects, providerRepos, incompatibleRepos = [] },
) {
+ // Normalizing structure to support legacy backend format
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091 for details
+
state.isLoadingRepos = false;
- state.importedProjects = importedProjects;
- state.providerRepos = providerRepos;
- state.incompatibleRepos = incompatibleRepos ?? [];
- state.namespaces = namespaces;
+ state.repositories = [
+ ...importedProjects.map(({ importSource, providerLink, importStatus, ...project }) => ({
+ importSource: {
+ id: `finished-${project.id}`,
+ fullName: importSource,
+ sanitizedName: project.name,
+ providerLink,
+ },
+ importStatus,
+ importedProject: project,
+ })),
+ ...providerRepos.map(project => ({
+ importSource: project,
+ importStatus: STATUSES.NONE,
+ importedProject: null,
+ })),
+ ...incompatibleRepos.map(project => ({
+ importSource: { ...project, incompatible: true },
+ importStatus: STATUSES.NONE,
+ importedProject: null,
+ })),
+ ];
},
[types.RECEIVE_REPOS_ERROR](state) {
state.isLoadingRepos = false;
},
- [types.REQUEST_IMPORT](state, repoId) {
- state.reposBeingImported.push(repoId);
+ [types.REQUEST_IMPORT](state, { repoId, importTarget }) {
+ const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
+ existingRepo.importStatus = STATUSES.SCHEDULING;
+ existingRepo.importedProject = {
+ fullPath: `/${importTarget.targetNamespace}/${importTarget.newName}`,
+ };
},
[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }) {
- const existingRepoIndex = state.reposBeingImported.indexOf(repoId);
- if (state.reposBeingImported.includes(repoId))
- state.reposBeingImported.splice(existingRepoIndex, 1);
+ const { importStatus, ...project } = importedProject;
- const providerRepoIndex = state.providerRepos.findIndex(
- providerRepo => providerRepo.id === repoId,
- );
- state.providerRepos.splice(providerRepoIndex, 1);
- state.importedProjects.unshift(importedProject);
+ const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
+ existingRepo.importStatus = importStatus;
+ existingRepo.importedProject = project;
},
[types.RECEIVE_IMPORT_ERROR](state, repoId) {
- const repoIndex = state.reposBeingImported.indexOf(repoId);
- if (state.reposBeingImported.includes(repoId)) state.reposBeingImported.splice(repoIndex, 1);
+ const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
+ existingRepo.importStatus = STATUSES.NONE;
+ existingRepo.importedProject = null;
},
[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects) {
updatedProjects.forEach(updatedProject => {
- const existingProject = state.importedProjects.find(
- importedProject => importedProject.id === updatedProject.id,
- );
-
- Vue.set(existingProject, 'importStatus', updatedProject.importStatus);
+ const repo = state.repositories.find(p => p.importedProject?.id === updatedProject.id);
+ if (repo) {
+ repo.importStatus = updatedProject.importStatus;
+ }
});
},
+
+ [types.REQUEST_NAMESPACES](state) {
+ state.isLoadingNamespaces = true;
+ },
+
+ [types.RECEIVE_NAMESPACES_SUCCESS](state, namespaces) {
+ state.isLoadingNamespaces = false;
+ state.namespaces = namespaces;
+ },
+
+ [types.RECEIVE_NAMESPACES_ERROR](state) {
+ state.isLoadingNamespaces = false;
+ },
+
+ [types.SET_IMPORT_TARGET](state, { repoId, importTarget }) {
+ const existingRepo = state.repositories.find(r => r.importSource.id === repoId);
+
+ if (
+ importTarget.targetNamespace === state.defaultTargetNamespace &&
+ importTarget.newName === existingRepo.importSource.sanitizedName
+ ) {
+ Vue.delete(state.customImportTargets, repoId);
+ } else {
+ Vue.set(state.customImportTargets, repoId, importTarget);
+ }
+ },
+
+ [types.SET_PAGE_INFO](state, pageInfo) {
+ state.pageInfo = pageInfo;
+ },
+
+ [types.SET_PAGE](state, page) {
+ state.pageInfo.page = page;
+ },
};
diff --git a/app/assets/javascripts/import_projects/store/state.js b/app/assets/javascripts/import_projects/store/state.js
index 0418d735b1d..3318181e4af 100644
--- a/app/assets/javascripts/import_projects/store/state.js
+++ b/app/assets/javascripts/import_projects/store/state.js
@@ -1,17 +1,13 @@
export default () => ({
- reposPath: '',
- importPath: '',
- jobsPath: '',
- currentProjectId: '',
provider: '',
- currentUsername: '',
- importedProjects: [],
- providerRepos: [],
- incompatibleRepos: [],
+ repositories: [],
namespaces: [],
- reposBeingImported: [],
+ customImportTargets: {},
isLoadingRepos: false,
- canSelectNamespace: false,
+ isLoadingNamespaces: false,
ciCdOnly: false,
filter: '',
+ pageInfo: {
+ page: 1,
+ },
});
diff --git a/app/assets/javascripts/import_projects/utils.js b/app/assets/javascripts/import_projects/utils.js
new file mode 100644
index 00000000000..c2a2d5a607d
--- /dev/null
+++ b/app/assets/javascripts/import_projects/utils.js
@@ -0,0 +1,7 @@
+import { STATUSES } from './constants';
+
+// Will be expanded in future
+// eslint-disable-next-line import/prefer-default-export
+export function isProjectImportable(project) {
+ return project.importStatus === STATUSES.NONE && !project.importSource.incompatible;
+}
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index f44c5c3d289..349ca14b4e8 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import { escape } from 'lodash';
import { __, sprintf } from './locale';
import axios from './lib/utils/axios_utils';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import { parseBoolean } from './lib/utils/common_utils';
class ImporterStatus {
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
new file mode 100644
index 00000000000..46852e4ddd9
--- /dev/null
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -0,0 +1,407 @@
+<script>
+import {
+ GlLoadingIcon,
+ GlTable,
+ GlAlert,
+ GlAvatarsInline,
+ GlAvatarLink,
+ GlAvatar,
+ GlTooltipDirective,
+ GlButton,
+ GlSearchBoxByType,
+ GlIcon,
+ GlPagination,
+ GlTabs,
+ GlTab,
+ GlBadge,
+ GlEmptyState,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { convertToSnakeCase } from '~/lib/utils/text_utility';
+import { s__ } from '~/locale';
+import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
+import getIncidents from '../graphql/queries/get_incidents.query.graphql';
+import getIncidentsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
+import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATUS_TABS } from '../constants';
+
+const TH_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
+const tdClass =
+ 'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
+const thClass = 'gl-hover-bg-blue-50';
+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';
+
+const initialPaginationState = {
+ currentPage: 1,
+ prevPageCursor: '',
+ nextPageCursor: '',
+ firstPageSize: DEFAULT_PAGE_SIZE,
+ lastPageSize: null,
+};
+
+export default {
+ i18n: I18N,
+ statusTabs: INCIDENT_STATUS_TABS,
+ fields: [
+ {
+ key: 'title',
+ label: s__('IncidentManagement|Incident'),
+ thClass: `gl-pointer-events-none gl-w-half`,
+ tdClass,
+ },
+ {
+ key: 'createdAt',
+ label: s__('IncidentManagement|Date created'),
+ thClass,
+ tdClass: `${tdClass} sortable-cell`,
+ sortable: true,
+ thAttr: TH_TEST_ID,
+ },
+ {
+ key: 'assignees',
+ label: s__('IncidentManagement|Assignees'),
+ thClass: 'gl-pointer-events-none',
+ tdClass,
+ },
+ ],
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ GlAlert,
+ GlAvatarsInline,
+ GlAvatarLink,
+ GlAvatar,
+ GlButton,
+ TimeAgoTooltip,
+ GlSearchBoxByType,
+ GlIcon,
+ GlPagination,
+ GlTabs,
+ GlTab,
+ PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'),
+ GlBadge,
+ GlEmptyState,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: [
+ 'projectPath',
+ 'newIssuePath',
+ 'incidentTemplateName',
+ 'incidentType',
+ 'issuePath',
+ 'publishedAvailable',
+ 'emptyListSvgPath',
+ ],
+ apollo: {
+ incidents: {
+ query: getIncidents,
+ variables() {
+ return {
+ searchTerm: this.searchTerm,
+ status: this.statusFilter,
+ projectPath: this.projectPath,
+ issueTypes: ['INCIDENT'],
+ sort: this.sort,
+ firstPageSize: this.pagination.firstPageSize,
+ lastPageSize: this.pagination.lastPageSize,
+ prevPageCursor: this.pagination.prevPageCursor,
+ nextPageCursor: this.pagination.nextPageCursor,
+ };
+ },
+ update({ project: { issues: { nodes = [], pageInfo = {} } = {} } = {} }) {
+ return {
+ list: nodes,
+ pageInfo,
+ };
+ },
+ error() {
+ this.errored = true;
+ },
+ },
+ incidentsCount: {
+ query: getIncidentsCountByStatus,
+ variables() {
+ return {
+ searchTerm: this.searchTerm,
+ projectPath: this.projectPath,
+ issueTypes: ['INCIDENT'],
+ };
+ },
+ update(data) {
+ return data.project?.issueStatusCounts;
+ },
+ },
+ },
+ data() {
+ return {
+ errored: false,
+ isErrorAlertDismissed: false,
+ redirecting: false,
+ searchTerm: '',
+ pagination: initialPaginationState,
+ incidents: {},
+ sort: 'created_desc',
+ sortBy: 'createdAt',
+ sortDesc: true,
+ statusFilter: '',
+ filteredByStatus: '',
+ };
+ },
+ computed: {
+ showErrorMsg() {
+ return this.errored && !this.isErrorAlertDismissed;
+ },
+ loading() {
+ return this.$apollo.queries.incidents.loading;
+ },
+ hasIncidents() {
+ return this.incidents?.list?.length;
+ },
+ incidentsForCurrentTab() {
+ return this.incidentsCount?.[this.filteredByStatus.toLowerCase()] ?? 0;
+ },
+ showPaginationControls() {
+ return Boolean(
+ this.incidents?.pageInfo?.hasNextPage || this.incidents?.pageInfo?.hasPreviousPage,
+ );
+ },
+ prevPage() {
+ return Math.max(this.pagination.currentPage - 1, 0);
+ },
+ nextPage() {
+ const nextPage = this.pagination.currentPage + 1;
+ return nextPage > Math.ceil(this.incidentsForCurrentTab / DEFAULT_PAGE_SIZE)
+ ? null
+ : nextPage;
+ },
+ tbodyTrClass() {
+ return {
+ [bodyTrClass]: !this.loading && this.hasIncidents,
+ };
+ },
+ newIncidentPath() {
+ return mergeUrlParams(
+ {
+ issuable_template: this.incidentTemplateName,
+ 'issue[issue_type]': this.incidentType,
+ },
+ this.newIssuePath,
+ );
+ },
+ availableFields() {
+ return this.publishedAvailable
+ ? [
+ ...this.$options.fields,
+ ...[
+ {
+ key: 'published',
+ label: s__('IncidentManagement|Published'),
+ thClass: 'gl-pointer-events-none',
+ },
+ ],
+ ]
+ : this.$options.fields;
+ },
+ isEmpty() {
+ return !this.incidents.list?.length;
+ },
+ },
+ methods: {
+ onInputChange: debounce(function debounceSearch(input) {
+ const trimmedInput = input.trim();
+ if (trimmedInput !== this.searchTerm) {
+ this.searchTerm = trimmedInput;
+ }
+ }, INCIDENT_SEARCH_DELAY),
+ filterIncidentsByStatus(tabIndex) {
+ const { filters, status } = this.$options.statusTabs[tabIndex];
+ this.statusFilter = filters;
+ this.filteredByStatus = status;
+ },
+ hasAssignees(assignees) {
+ return Boolean(assignees.nodes?.length);
+ },
+ navigateToIncidentDetails({ iid }) {
+ return visitUrl(joinPaths(this.issuePath, iid));
+ },
+ handlePageChange(page) {
+ const { startCursor, endCursor } = this.incidents.pageInfo;
+
+ if (page > this.pagination.currentPage) {
+ this.pagination = {
+ ...initialPaginationState,
+ nextPageCursor: endCursor,
+ currentPage: page,
+ };
+ } else {
+ this.pagination = {
+ lastPageSize: DEFAULT_PAGE_SIZE,
+ firstPageSize: null,
+ prevPageCursor: startCursor,
+ nextPageCursor: '',
+ currentPage: page,
+ };
+ }
+ },
+ resetPagination() {
+ this.pagination = initialPaginationState;
+ },
+ fetchSortedData({ sortBy, sortDesc }) {
+ const sortingDirection = sortDesc ? 'desc' : 'asc';
+ const sortingColumn = convertToSnakeCase(sortBy).replace(/_.*/, '');
+
+ this.sort = `${sortingColumn}_${sortingDirection}`;
+ },
+ },
+};
+</script>
+<template>
+ <div class="incident-management-list">
+ <gl-alert v-if="showErrorMsg" variant="danger" @dismiss="isErrorAlertDismissed = true">
+ {{ $options.i18n.errorMsg }}
+ </gl-alert>
+
+ <div
+ class="incident-management-list-header gl-display-flex gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-gray-100"
+ >
+ <gl-tabs content-class="gl-p-0" @input="filterIncidentsByStatus">
+ <gl-tab v-for="tab in $options.statusTabs" :key="tab.status" :data-testid="tab.status">
+ <template #title>
+ <span>{{ tab.title }}</span>
+ <gl-badge v-if="incidentsCount" pill size="sm" class="gl-tab-counter-badge">
+ {{ incidentsCount[tab.status.toLowerCase()] }}
+ </gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+
+ <gl-button
+ v-if="!isEmpty"
+ class="gl-my-3 gl-mr-5 create-incident-button"
+ data-testid="createIncidentBtn"
+ data-qa-selector="create_incident_button"
+ :loading="redirecting"
+ :disabled="redirecting"
+ category="primary"
+ variant="success"
+ :href="newIncidentPath"
+ @click="redirecting = true"
+ >
+ {{ $options.i18n.createIncidentBtnLabel }}
+ </gl-button>
+ </div>
+
+ <div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100">
+ <gl-search-box-by-type
+ :value="searchTerm"
+ class="gl-bg-white"
+ :placeholder="$options.i18n.searchPlaceholder"
+ @input="onInputChange"
+ />
+ </div>
+
+ <h4 class="gl-display-block d-md-none my-3">
+ {{ s__('IncidentManagement|Incidents') }}
+ </h4>
+ <gl-table
+ :items="incidents.list || []"
+ :fields="availableFields"
+ :show-empty="true"
+ :busy="loading"
+ stacked="md"
+ :tbody-tr-class="tbodyTrClass"
+ :no-local-sorting="true"
+ :sort-direction="'desc'"
+ :sort-desc.sync="sortDesc"
+ :sort-by.sync="sortBy"
+ sort-icon-left
+ fixed
+ @row-clicked="navigateToIncidentDetails"
+ @sort-changed="fetchSortedData"
+ >
+ <template #cell(title)="{ item }">
+ <div :class="{ 'gl-display-flex gl-align-items-center': item.state === 'closed' }">
+ <div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
+ <gl-icon
+ v-if="item.state === 'closed'"
+ name="issue-close"
+ class="gl-mx-1 gl-fill-blue-500 gl-flex-shrink-0"
+ :size="16"
+ data-testid="incident-closed"
+ />
+ </div>
+ </template>
+
+ <template #cell(createdAt)="{ item }">
+ <time-ago-tooltip :time="item.createdAt" />
+ </template>
+
+ <template #cell(assignees)="{ item }">
+ <div data-testid="incident-assignees">
+ <template v-if="hasAssignees(item.assignees)">
+ <gl-avatars-inline
+ :avatars="item.assignees.nodes"
+ :collapsed="true"
+ :max-visible="4"
+ :avatar-size="24"
+ badge-tooltip-prop="name"
+ :badge-tooltip-max-chars="100"
+ >
+ <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="24" />
+ </gl-avatar-link>
+ </template>
+ </gl-avatars-inline>
+ </template>
+ <template v-else>
+ {{ $options.i18n.unassigned }}
+ </template>
+ </div>
+ </template>
+
+ <template v-if="publishedAvailable" #cell(published)="{ item }">
+ <published-cell
+ :status-page-published-incident="item.statusPagePublishedIncident"
+ :un-published="$options.i18n.unPublished"
+ />
+ </template>
+ <template #table-busy>
+ <gl-loading-icon size="lg" color="dark" class="mt-3" />
+ </template>
+
+ <template #empty>
+ <gl-empty-state
+ v-if="!errored"
+ :title="$options.i18n.emptyState.title"
+ :svg-path="emptyListSvgPath"
+ :description="$options.i18n.emptyState.description"
+ :primary-button-link="newIncidentPath"
+ :primary-button-text="$options.i18n.createIncidentBtnLabel"
+ />
+ <span v-else>
+ {{ $options.i18n.noIncidents }}
+ </span>
+ </template>
+ </gl-table>
+
+ <gl-pagination
+ v-if="showPaginationControls"
+ :value="pagination.currentPage"
+ :prev-page="prevPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-pagination gl-mt-3"
+ @input="handlePageChange"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js
new file mode 100644
index 00000000000..02d8172533d
--- /dev/null
+++ b/app/assets/javascripts/incidents/constants.js
@@ -0,0 +1,37 @@
+import { s__, __ } from '~/locale';
+
+export const I18N = {
+ errorMsg: s__('IncidentManagement|There was an error displaying the incidents.'),
+ noIncidents: s__('IncidentManagement|No incidents to display.'),
+ unassigned: s__('IncidentManagement|Unassigned'),
+ createIncidentBtnLabel: s__('IncidentManagement|Create incident'),
+ unPublished: s__('IncidentManagement|Unpublished'),
+ searchPlaceholder: __('Search results…'),
+ emptyState: {
+ title: s__('IncidentManagement|Display your incidents in a dedicated view'),
+ description: s__(
+ 'IncidentManagement|All alerts promoted to incidents will automatically be displayed within the list. You can also create a new incident using the button below.',
+ ),
+ },
+};
+
+export const INCIDENT_STATUS_TABS = [
+ {
+ title: s__('IncidentManagement|Open'),
+ status: 'OPENED',
+ filters: 'opened',
+ },
+ {
+ title: s__('IncidentManagement|Closed'),
+ status: 'CLOSED',
+ filters: 'closed',
+ },
+ {
+ title: s__('IncidentManagement|All'),
+ status: 'ALL',
+ filters: 'all',
+ },
+];
+
+export const INCIDENT_SEARCH_DELAY = 300;
+export const DEFAULT_PAGE_SIZE = 10;
diff --git a/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql
new file mode 100644
index 00000000000..0b784b104a8
--- /dev/null
+++ b/app/assets/javascripts/incidents/graphql/queries/get_count_by_status.query.graphql
@@ -0,0 +1,9 @@
+query getIncidentsCountByStatus($searchTerm: String, $projectPath: ID!, $issueTypes: [IssueType!]) {
+ project(fullPath: $projectPath) {
+ issueStatusCounts(search: $searchTerm, types: $issueTypes) {
+ all
+ opened
+ closed
+ }
+ }
+}
diff --git a/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
new file mode 100644
index 00000000000..0f56e8640bd
--- /dev/null
+++ b/app/assets/javascripts/incidents/graphql/queries/get_incidents.query.graphql
@@ -0,0 +1,52 @@
+query getIncidents(
+ $projectPath: ID!
+ $issueTypes: [IssueType!]
+ $sort: IssueSort
+ $status: IssuableState
+ $firstPageSize: Int
+ $lastPageSize: Int
+ $prevPageCursor: String = ""
+ $nextPageCursor: String = ""
+ $searchTerm: String
+) {
+ project(fullPath: $projectPath) {
+ issues(
+ search: $searchTerm
+ types: $issueTypes
+ sort: $sort
+ state: $status
+ first: $firstPageSize
+ last: $lastPageSize
+ after: $nextPageCursor
+ before: $prevPageCursor
+ ) {
+ nodes {
+ iid
+ title
+ createdAt
+ state
+ labels {
+ nodes {
+ title
+ color
+ }
+ }
+ assignees {
+ nodes {
+ name
+ username
+ avatarUrl
+ webUrl
+ }
+ }
+ statusPagePublishedIncident
+ }
+ pageInfo {
+ hasNextPage
+ endCursor
+ hasPreviousPage
+ startCursor
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/incidents/list.js b/app/assets/javascripts/incidents/list.js
new file mode 100644
index 00000000000..7505d07449c
--- /dev/null
+++ b/app/assets/javascripts/incidents/list.js
@@ -0,0 +1,44 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import IncidentsList from './components/incidents_list.vue';
+
+Vue.use(VueApollo);
+export default () => {
+ const selector = '#js-incidents';
+
+ const domEl = document.querySelector(selector);
+ const {
+ projectPath,
+ newIssuePath,
+ incidentTemplateName,
+ incidentType,
+ issuePath,
+ publishedAvailable,
+ emptyListSvgPath,
+ } = domEl.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el: selector,
+ provide: {
+ projectPath,
+ incidentTemplateName,
+ incidentType,
+ newIssuePath,
+ issuePath,
+ publishedAvailable,
+ emptyListSvgPath,
+ },
+ apolloProvider,
+ components: {
+ IncidentsList,
+ },
+ render(createElement) {
+ return createElement('incidents-list');
+ },
+ });
+};
diff --git a/app/assets/javascripts/incidents_settings/components/alerts_form.vue b/app/assets/javascripts/incidents_settings/components/alerts_form.vue
index a394f404ee1..5872ac39c96 100644
--- a/app/assets/javascripts/incidents_settings/components/alerts_form.vue
+++ b/app/assets/javascripts/incidents_settings/components/alerts_form.vue
@@ -123,17 +123,18 @@ export default {
<span>{{ $options.i18n.sendEmail.label }}</span>
</gl-form-checkbox>
</gl-form-group>
-
- <gl-button
- ref="submitBtn"
- data-qa-selector="save_changes_button"
- :disabled="loading"
- variant="success"
- type="submit"
- class="js-no-auto-disable"
- >
- {{ $options.i18n.saveBtnLabel }}
- </gl-button>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button
+ ref="submitBtn"
+ data-qa-selector="save_changes_button"
+ :disabled="loading"
+ variant="success"
+ type="submit"
+ class="js-no-auto-disable"
+ >
+ {{ $options.i18n.saveBtnLabel }}
+ </gl-button>
+ </div>
</form>
</div>
</template>
diff --git a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
index 0623c275c5a..d6e963c6f4f 100644
--- a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
+++ b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
@@ -2,7 +2,6 @@
import { GlButton, GlTabs, GlTab } from '@gitlab/ui';
import AlertsSettingsForm from './alerts_form.vue';
import PagerDutySettingsForm from './pagerduty_form.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { INTEGRATION_TABS_CONFIG, I18N_INTEGRATION_TABS } from '../constants';
export default {
@@ -13,17 +12,8 @@ export default {
AlertsSettingsForm,
PagerDutySettingsForm,
},
- mixins: [glFeatureFlagMixin()],
tabs: INTEGRATION_TABS_CONFIG,
i18n: I18N_INTEGRATION_TABS,
- methods: {
- isFeatureFlagEnabled(tab) {
- if (tab.featureFlag) {
- return this.glFeatures[tab.featureFlag];
- }
- return true;
- },
- },
};
</script>
@@ -34,9 +24,9 @@ export default {
class="settings no-animate qa-incident-management-settings"
>
<div class="settings-header">
- <h3 ref="sectionHeader" class="h4">
+ <h4 ref="sectionHeader" class="gl-my-3! gl-py-1">
{{ $options.i18n.headerText }}
- </h3>
+ </h4>
<gl-button ref="toggleBtn" class="js-settings-toggle">{{
$options.i18n.expandBtnLabel
}}</gl-button>
@@ -49,7 +39,7 @@ export default {
<gl-tabs>
<gl-tab
v-for="(tab, index) in $options.tabs"
- v-if="tab.active && isFeatureFlagEnabled(tab)"
+ v-if="tab.active"
:key="`${tab.title}_${index}`"
:title="tab.title"
>
diff --git a/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
index 027848db6e9..8b608d9f391 100644
--- a/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
+++ b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
@@ -11,9 +11,9 @@ import {
GlModal,
GlModalDirective,
} from '@gitlab/ui';
+import { isEqual } from 'lodash';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { I18N_PAGERDUTY_SETTINGS_FORM, CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK } from '../constants';
-import { isEqual } from 'lodash';
export default {
components: {
@@ -135,7 +135,7 @@ export default {
</template>
</gl-form-input-group>
- <div class="gl-text-gray-400 gl-pt-2">
+ <div class="gl-text-gray-200 gl-pt-2">
<gl-sprintf :message="$options.i18n.webhookUrl.helpText">
<template #docsLink>
<gl-link
@@ -149,15 +149,17 @@ export default {
</template>
</gl-sprintf>
</div>
- <gl-button
- v-gl-modal.resetWebhookModal
- class="gl-mt-3"
- :disabled="loading"
- :loading="resettingWebhook"
- data-testid="webhook-reset-btn"
- >
- {{ $options.i18n.webhookUrl.resetWebhookUrl }}
- </gl-button>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button
+ v-gl-modal.resetWebhookModal
+ class="gl-mt-3"
+ :disabled="loading"
+ :loading="resettingWebhook"
+ data-testid="webhook-reset-btn"
+ >
+ {{ $options.i18n.webhookUrl.resetWebhookUrl }}
+ </gl-button>
+ </div>
<gl-modal
modal-id="resetWebhookModal"
:title="$options.i18n.webhookUrl.resetWebhookUrl"
@@ -168,16 +170,17 @@ export default {
{{ $options.i18n.webhookUrl.restKeyInfo }}
</gl-modal>
</gl-form-group>
-
- <gl-button
- ref="submitBtn"
- :disabled="isSaveDisabled"
- variant="success"
- type="submit"
- class="js-no-auto-disable"
- >
- {{ $options.i18n.saveBtnLabel }}
- </gl-button>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button
+ ref="submitBtn"
+ :disabled="isSaveDisabled"
+ variant="success"
+ type="submit"
+ class="js-no-auto-disable"
+ >
+ {{ $options.i18n.saveBtnLabel }}
+ </gl-button>
+ </div>
</form>
</div>
</template>
diff --git a/app/assets/javascripts/incidents_settings/constants.js b/app/assets/javascripts/incidents_settings/constants.js
index b443c237f0f..77f7ee2c4a3 100644
--- a/app/assets/javascripts/incidents_settings/constants.js
+++ b/app/assets/javascripts/incidents_settings/constants.js
@@ -11,7 +11,6 @@ export const INTEGRATION_TABS_CONFIG = [
title: s__('IncidentSettings|PagerDuty integration'),
component: 'PagerDutySettingsForm',
active: true,
- featureFlag: 'pagerdutyWebhook',
},
{
title: s__('IncidentSettings|Grafana integration'),
@@ -47,7 +46,7 @@ export const I18N_ALERT_SETTINGS_FORM = {
export const NO_ISSUE_TEMPLATE_SELECTED = { key: '', name: __('No template selected') };
export const TAKING_INCIDENT_ACTION_DOCS_LINK =
- '/help/user/project/integrations/prometheus#taking-action-on-incidents-ultimate';
+ '/help/operations/metrics/alerts#trigger-actions-from-alerts';
export const ISSUE_TEMPLATES_DOCS_LINK =
'/help/user/project/description_templates#creating-issue-templates';
diff --git a/app/assets/javascripts/incidents_settings/incidents_settings_service.js b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
index bd4f5bb8820..f0e692f9cbe 100644
--- a/app/assets/javascripts/incidents_settings/incidents_settings_service.js
+++ b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { ERROR_MSG } from './constants';
export default class IncidentsSettingsService {
diff --git a/app/assets/javascripts/integrations/edit/components/active_toggle.vue b/app/assets/javascripts/integrations/edit/components/active_toggle.vue
index a3087c8958e..e6a96600539 100644
--- a/app/assets/javascripts/integrations/edit/components/active_toggle.vue
+++ b/app/assets/javascripts/integrations/edit/components/active_toggle.vue
@@ -1,8 +1,7 @@
<script>
import { mapGetters } from 'vuex';
-import eventHub from '../event_hub';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { GlFormGroup, GlToggle } from '@gitlab/ui';
+import eventHub from '../event_hub';
export default {
name: 'ActiveToggle',
@@ -10,7 +9,6 @@ export default {
GlFormGroup,
GlToggle,
},
- mixins: [glFeatureFlagsMixin()],
props: {
initialActivated: {
type: Boolean,
@@ -40,28 +38,13 @@ export default {
</script>
<template>
- <div v-if="glFeatures.integrationFormRefactor">
- <gl-form-group :label="__('Enable integration')" label-for="service[active]">
- <gl-toggle
- v-model="activated"
- name="service[active]"
- class="gl-display-block gl-line-height-0"
- :disabled="isInheriting"
- @change="onToggle"
- />
- </gl-form-group>
- </div>
- <div v-else>
- <div class="form-group row" role="group">
- <label for="service[active]" class="col-form-label col-sm-2">{{ __('Active') }}</label>
- <div class="col-sm-10 pt-1">
- <gl-toggle
- v-model="activated"
- name="service[active]"
- :disabled="isInheriting"
- @change="onToggle"
- />
- </div>
- </div>
- </div>
+ <gl-form-group :label="__('Enable integration')" label-for="service[active]">
+ <gl-toggle
+ v-model="activated"
+ name="service[active]"
+ class="gl-display-block gl-line-height-0"
+ :disabled="isInheriting"
+ @change="onToggle"
+ />
+ </gl-form-group>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index 6053d11e6da..090381b8da4 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -1,9 +1,9 @@
<script>
import { mapGetters } from 'vuex';
-import eventHub from '../event_hub';
import { capitalize, lowerCase, isEmpty } from 'lodash';
-import { __, sprintf } from '~/locale';
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
+import eventHub from '../event_hub';
+import { __, sprintf } from '~/locale';
export default {
name: 'DynamicField',
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index 5444cd5a712..5a1f86718b0 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -1,5 +1,4 @@
<script>
-import eventHub from '../event_hub';
import {
GlFormGroup,
GlFormCheckbox,
@@ -9,6 +8,7 @@ import {
GlButton,
GlCard,
} from '@gitlab/ui';
+import eventHub from '../event_hub';
export default {
name: 'JiraIssuesFields',
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
index 1d3354c6651..08f24ce8ab6 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
@@ -1,8 +1,7 @@
<script>
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { mapGetters } from 'vuex';
-import { s__ } from '~/locale';
import { GlFormGroup, GlFormCheckbox, GlFormRadio } from '@gitlab/ui';
+import { s__ } from '~/locale';
const commentDetailOptions = [
{
@@ -26,7 +25,6 @@ export default {
GlFormCheckbox,
GlFormRadio,
},
- mixins: [glFeatureFlagsMixin()],
props: {
initialTriggerCommit: {
type: Boolean,
@@ -65,7 +63,7 @@ export default {
</script>
<template>
- <div v-if="glFeatures.integrationFormRefactor">
+ <div>
<gl-form-group
:label="__('Trigger')"
label-for="service[trigger]"
@@ -130,73 +128,4 @@ export default {
</gl-form-radio>
</gl-form-group>
</div>
-
- <div v-else class="form-group row pt-2" role="group">
- <label for="service[trigger]" class="col-form-label col-sm-2 pt-0">{{ __('Trigger') }}</label>
- <div class="col-sm-10">
- <label class="weight-normal mb-2">
- {{
- s__(
- 'Integrations|When a Jira issue is mentioned in a commit or merge request a remote link and comment (if enabled) will be created.',
- )
- }}
- </label>
-
- <input name="service[commit_events]" type="hidden" :value="triggerCommit || false" />
- <gl-form-checkbox v-model="triggerCommit" :disabled="isInheriting">
- {{ __('Commit') }}
- </gl-form-checkbox>
-
- <input
- name="service[merge_requests_events]"
- type="hidden"
- :value="triggerMergeRequest || false"
- />
- <gl-form-checkbox v-model="triggerMergeRequest" :disabled="isInheriting">
- {{ __('Merge request') }}
- </gl-form-checkbox>
-
- <div
- v-show="triggerCommit || triggerMergeRequest"
- class="mt-4"
- data-testid="comment-settings"
- >
- <label>
- {{ s__('Integrations|Comment settings:') }}
- </label>
- <input
- name="service[comment_on_event_enabled]"
- type="hidden"
- :value="enableComments || false"
- />
- <gl-form-checkbox v-model="enableComments" :disabled="isInheriting">
- {{ s__('Integrations|Enable comments') }}
- </gl-form-checkbox>
-
- <div v-show="enableComments" class="mt-4" data-testid="comment-detail">
- <label>
- {{ s__('Integrations|Comment detail:') }}
- </label>
- <input
- v-if="isInheriting"
- name="service[comment_detail]"
- type="hidden"
- :value="commentDetail"
- />
- <gl-form-radio
- v-for="commentDetailOption in commentDetailOptions"
- :key="commentDetailOption.value"
- v-model="commentDetail"
- :value="commentDetailOption.value"
- :disabled="isInheriting"
- >
- {{ commentDetailOption.label }}
- <template #help>
- {{ commentDetailOption.help }}
- </template>
- </gl-form-radio>
- </div>
- </div>
- </div>
- </div>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
index 0ae2f267434..accfc26974c 100644
--- a/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
+++ b/app/assets/javascripts/integrations/edit/components/override_dropdown.vue
@@ -1,11 +1,11 @@
<script>
-import { s__ } from '~/locale';
import { GlNewDropdown, GlNewDropdownItem } from '@gitlab/ui';
+import { s__ } from '~/locale';
const dropdownOptions = [
{
value: false,
- text: s__('Integrations|Use instance level settings'),
+ text: s__('Integrations|Use default settings'),
},
{
value: true,
@@ -48,7 +48,7 @@ export default {
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-py-4 gl-mt-5 gl-mb-6 gl-border-t-1 gl-border-t-solid gl-border-b-1 gl-border-b-solid gl-border-gray-100"
>
- <span>{{ s__('Integrations|This integration has multiple settings available.') }}</span>
+ <span>{{ s__('Integrations|Default settings are inherited from the instance level.') }}</span>
<input name="service[inherit_from_id]" :value="override ? '' : inheritFromId" type="hidden" />
<gl-new-dropdown :text="selected.text">
<gl-new-dropdown-item
diff --git a/app/assets/javascripts/integrations/edit/components/trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
index bb1e0d9d360..32878c6afa4 100644
--- a/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters } from 'vuex';
import { startCase } from 'lodash';
-import { __ } from '~/locale';
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
+import { __ } from '~/locale';
const typeWithPlaceholder = {
SLACK: 'slack',
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 837409a91ca..1135065b06c 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import axios from '../lib/utils/axios_utils';
-import flash from '../flash';
+import { deprecatedCreateFlash as flash } from '../flash';
import { __ } from '~/locale';
import initForm from './edit';
import eventHub from './edit/event_hub';
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index d968e9e5235..c7806fc17fc 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { difference, intersection, union } from 'lodash';
import axios from './lib/utils/axios_utils';
-import Flash from './flash';
+import { deprecatedCreateFlash as Flash } from './flash';
import { __ } from './locale';
export default {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index cf780556c8d..2dcf5e6a0d6 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -48,7 +48,19 @@ export default class IssuableForm {
this.renderWipExplanation = this.renderWipExplanation.bind(this);
this.resetAutosave = this.resetAutosave.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
- this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
+ /* eslint-disable @gitlab/require-i18n-strings */
+ this.wipRegex = new RegExp(
+ '^\\s*(' + // Line start, then any amount of leading whitespace
+ 'draft\\s-\\s' + // Draft_-_ where "_" are *exactly* one whitespace
+ '|\\[(draft|wip)\\]\\s*' + // [Draft] or [WIP] and any following whitespace
+ '|(draft|wip):\\s*' + // Draft: or WIP: and any following whitespace
+ '|(draft|wip)\\s+' + // Draft_ or WIP_ where "_" is at least one whitespace
+ '|\\(draft\\)\\s*' + // (Draft) and any following whitespace
+ ')+' + // At least one repeated match of the preceding parenthetical
+ '\\s*', // Any amount of trailing whitespace
+ 'i', // Match any case(s)
+ );
+ /* eslint-enable @gitlab/require-i18n-strings */
this.gfmAutoComplete = new GfmAutoComplete(
gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
@@ -131,9 +143,18 @@ export default class IssuableForm {
workInProgress() {
return this.wipRegex.test(this.titleField.val());
}
+ titlePrefixContainsDraft() {
+ const prefix = this.titleField.val().match(this.wipRegex);
+
+ return prefix && prefix[0].match(/draft/i);
+ }
renderWipExplanation() {
if (this.workInProgress()) {
+ // These strings are not "translatable" (the code is hard-coded to look for them)
+ this.$wipExplanation.find('code')[0].textContent = this.titlePrefixContainsDraft()
+ ? 'Draft' /* eslint-disable-line @gitlab/require-i18n-strings */
+ : 'WIP';
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
}
@@ -156,7 +177,7 @@ export default class IssuableForm {
}
addWip() {
- this.titleField.val(`WIP: ${this.titleField.val()}`);
+ this.titleField.val(`Draft: ${this.titleField.val()}`);
}
initTargetBranchDropdown() {
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index f3f8b6ec715..e888e481fe5 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import { s__, __ } from './locale';
import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar';
diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue
index b7f4292a126..4fc614f8da4 100644
--- a/app/assets/javascripts/issuables_list/components/issuable.vue
+++ b/app/assets/javascripts/issuables_list/components/issuable.vue
@@ -23,6 +23,8 @@ import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import { isScopedLabel } from '~/lib/utils/common_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { convertToCamelCase } from '~/lib/utils/text_utility';
+
export default {
i18n: {
openedAgo: __('opened %{timeAgoString} by %{user}'),
@@ -34,6 +36,8 @@ export default {
GlLabel,
GlIcon,
GlSprintf,
+ IssueHealthStatus: () =>
+ import('ee_component/related_items_tree/components/issue_health_status.vue'),
},
directives: {
GlTooltip,
@@ -85,9 +89,6 @@ export default {
dueDateWords() {
return this.dueDate ? dateInWords(this.dueDate, true) : undefined;
},
- hasNoComments() {
- return !this.userNotesCount;
- },
isOverdue() {
return this.dueDate ? this.dueDate < new Date() : false;
},
@@ -148,34 +149,59 @@ export default {
time_ago: escape(getTimeago().format(this.issuable.updated_at)),
});
},
- userNotesCount() {
- return this.issuable.user_notes_count;
- },
issuableMeta() {
return [
{
key: 'merge-requests',
+ visible: this.issuable.merge_requests_count > 0,
value: this.issuable.merge_requests_count,
title: __('Related merge requests'),
- class: 'js-merge-requests',
+ dataTestId: 'merge-requests',
+ class: 'js-merge-requests icon-merge-request-unmerged',
icon: 'merge-request',
},
{
key: 'upvotes',
+ visible: this.issuable.upvotes > 0,
value: this.issuable.upvotes,
title: __('Upvotes'),
- class: 'js-upvotes',
+ dataTestId: 'upvotes',
+ class: 'js-upvotes issuable-upvotes',
icon: 'thumb-up',
},
{
key: 'downvotes',
+ visible: this.issuable.downvotes > 0,
value: this.issuable.downvotes,
title: __('Downvotes'),
- class: 'js-downvotes',
+ dataTestId: 'downvotes',
+ class: 'js-downvotes issuable-downvotes',
icon: 'thumb-down',
},
+ {
+ key: 'blocking-issues',
+ visible: this.issuable.blocking_issues_count > 0,
+ value: this.issuable.blocking_issues_count,
+ title: __('Blocking issues'),
+ dataTestId: 'blocking-issues',
+ href: `${this.issuable.web_url}#related-issues`,
+ icon: 'issue-block',
+ },
+ {
+ key: 'comments-count',
+ visible: !this.isJiraIssue,
+ value: this.issuable.user_notes_count,
+ title: __('Comments'),
+ dataTestId: 'notes-count',
+ href: `${this.issuable.web_url}#notes`,
+ class: { 'no-comments': !this.issuable.user_notes_count, 'issuable-comments': true },
+ icon: 'comments',
+ },
];
},
+ healthStatus() {
+ return convertToCamelCase(this.issuable.health_status);
+ },
},
mounted() {
// TODO: Refactor user popover to use its own component instead of
@@ -202,6 +228,9 @@ export default {
selected: ev.target.checked,
});
},
+ issuableMetaComponent(href) {
+ return href ? 'gl-link' : 'span';
+ },
},
confidentialTooltipText: __('Confidential'),
@@ -215,11 +244,14 @@ export default {
:data-id="issuable.id"
:data-labels="labelIdsString"
:data-url="issuable.web_url"
+ data-qa-selector="issue_container"
+ :data-qa-issue-title="issuable.title"
>
- <div class="d-flex">
+ <div class="gl-display-flex">
<!-- Bulk edit checkbox -->
- <div v-if="isBulkEditing" class="mr-2">
+ <div v-if="isBulkEditing" class="gl-mr-3">
<input
+ :id="`selected_issue_${issuable.id}`"
:checked="selected"
class="selected-issuable"
type="checkbox"
@@ -230,7 +262,7 @@ export default {
<!-- Issuable info container -->
<!-- Issuable main info -->
- <div class="flex-grow-1">
+ <div class="gl-flex-grow-1">
<div class="title">
<span class="issue-title-text">
<gl-icon
@@ -242,22 +274,28 @@ export default {
:title="$options.confidentialTooltipText"
:aria-label="$options.confidentialTooltipText"
/>
- <gl-link :href="issuable.web_url" :target="linkTarget" data-testid="issuable-title">
- {{ issuable.title }}
- <gl-icon
+ <gl-link
+ :href="issuable.web_url"
+ :target="linkTarget"
+ data-testid="issuable-title"
+ data-qa-selector="issue_link"
+ >{{ issuable.title
+ }}<gl-icon
v-if="isJiraIssue"
name="external-link"
- class="gl-vertical-align-text-bottom"
+ class="gl-vertical-align-text-bottom gl-ml-2"
/>
</gl-link>
</span>
- <span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block">
- {{ issuable.task_status }}
- </span>
+ <span
+ v-if="issuable.has_tasks"
+ class="gl-ml-2 task-status gl-display-none d-sm-inline-block"
+ >{{ issuable.task_status }}</span
+ >
</div>
<div class="issuable-info">
- <span class="js-ref-path">
+ <span class="js-ref-path gl-mr-4 mr-sm-0">
<span
v-if="isJiraIssue"
class="svg-container jira-logo-container"
@@ -267,7 +305,7 @@ export default {
{{ referencePath }}
</span>
- <span data-testid="openedByMessage" class="d-none d-sm-inline-block mr-1">
+ <span data-testid="openedByMessage" class="gl-display-none d-sm-inline-block gl-mr-4">
&middot;
<gl-sprintf
:message="isJiraIssue ? $options.i18n.openedAgoJira : $options.i18n.openedAgo"
@@ -281,9 +319,8 @@ export default {
v-bind="popoverDataAttrs"
:href="issuableAuthor.web_url"
:target="linkTarget"
+ >{{ issuableAuthor.name }}</gl-link
>
- {{ issuableAuthor.name }}
- </gl-link>
</template>
</gl-sprintf>
</span>
@@ -291,18 +328,18 @@ export default {
<gl-link
v-if="issuable.milestone"
v-gl-tooltip
- class="d-none d-sm-inline-block mr-1 js-milestone"
+ class="gl-display-none d-sm-inline-block gl-mr-4 js-milestone milestone"
:href="milestoneLink"
:title="milestoneTooltipText"
>
- <i class="fa fa-clock-o"></i>
+ <gl-icon name="clock" class="s16 gl-vertical-align-text-bottom" />
{{ issuable.milestone.title }}
</gl-link>
<span
v-if="dueDate"
v-gl-tooltip
- class="d-none d-sm-inline-block mr-1 js-due-date"
+ class="gl-display-none d-sm-inline-block gl-mr-4 js-due-date"
:class="{ cred: isOverdue }"
:title="__('Due date')"
>
@@ -310,6 +347,24 @@ export default {
{{ dueDateWords }}
</span>
+ <span
+ v-if="hasWeight"
+ v-gl-tooltip
+ :title="__('Weight')"
+ class="gl-display-none d-sm-inline-block gl-mr-4"
+ data-testid="weight"
+ data-qa-selector="issuable_weight_content"
+ >
+ <gl-icon name="weight" class="align-text-bottom" />
+ {{ issuable.weight }}
+ </span>
+
+ <issue-health-status
+ v-if="issuable.health_status"
+ :health-status="healthStatus"
+ class="gl-mr-4 issuable-tag-valign"
+ />
+
<gl-label
v-for="label in issuable.labels"
:key="label.id"
@@ -321,61 +376,44 @@ export default {
:title="label.name"
:scoped="isScoped(label)"
size="sm"
- class="mr-1"
+ class="gl-mr-2 issuable-tag-valign"
>{{ label.name }}</gl-label
>
-
- <span
- v-if="hasWeight"
- v-gl-tooltip
- :title="__('Weight')"
- class="d-none d-sm-inline-block js-weight"
- data-testid="weight"
- >
- <gl-icon name="weight" class="align-text-bottom" />
- {{ issuable.weight }}
- </span>
</div>
</div>
<!-- Issuable meta -->
- <div class="flex-shrink-0 d-flex flex-column align-items-end justify-content-center">
- <div class="controls d-flex">
+ <div
+ class="gl-flex-shrink-0 gl-display-flex gl-flex-direction-column align-items-end gl-justify-content-center"
+ >
+ <div class="controls gl-display-flex">
<span v-if="isJiraIssue" data-testid="issuable-status">{{ issuable.status }}</span>
<span v-else-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span>
<issue-assignees
:assignees="issuable.assignees"
- class="align-items-center d-flex ml-2"
+ class="gl-align-items-center gl-display-flex gl-ml-3"
:icon-size="16"
- img-css-classes="mr-1"
+ img-css-classes="gl-mr-2!"
:max-visible="4"
/>
<template v-for="meta in issuableMeta">
<span
- v-if="meta.value"
+ v-if="meta.visible"
:key="meta.key"
v-gl-tooltip
- :class="['d-none d-sm-inline-block ml-2 vertical-align-middle', meta.class]"
+ class="gl-display-none gl-display-sm-flex gl-align-items-center gl-ml-3"
+ :class="meta.class"
+ :data-testid="meta.dataTestId"
:title="meta.title"
>
- <gl-icon v-if="meta.icon" :name="meta.icon" />
- {{ meta.value }}
+ <component :is="issuableMetaComponent(meta.href)" :href="meta.href">
+ <gl-icon v-if="meta.icon" :name="meta.icon" />
+ {{ meta.value }}
+ </component>
</span>
</template>
-
- <gl-link
- v-if="!isJiraIssue"
- v-gl-tooltip
- class="ml-2 js-notes"
- :href="`${issuable.web_url}#notes`"
- :title="__('Comments')"
- :class="{ 'no-comments': hasNoComments }"
- >
- <gl-icon name="comments" class="gl-vertical-align-text-bottom" />
- {{ userNotesCount }}
- </gl-link>
</div>
<div v-gl-tooltip class="issuable-updated-at" :title="updatedDateString">
{{ updatedDateAgo }}
diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue
index 21aeb2ca143..fecb7353efb 100644
--- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue
+++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue
@@ -1,7 +1,12 @@
<script>
import { toNumber, omit } from 'lodash';
-import { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui';
-import flash from '~/flash';
+import {
+ GlEmptyState,
+ GlPagination,
+ GlSkeletonLoading,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
+import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
scrollToElement,
@@ -23,9 +28,13 @@ import {
} from '../constants';
import { setUrlParams } from '~/lib/utils/url_utility';
import issueableEventHub from '../eventhub';
+import { emptyStateHelper } from '../service_desk_helper';
export default {
LOADING_LIST_ITEMS_LENGTH,
+ directives: {
+ SafeHtml,
+ },
components: {
GlEmptyState,
GlPagination,
@@ -39,15 +48,9 @@ export default {
required: false,
default: false,
},
- createIssuePath: {
- type: String,
- required: false,
- default: '',
- },
- emptySvgPath: {
- type: String,
- required: false,
- default: '',
+ emptyStateMeta: {
+ type: Object,
+ required: true,
},
endpoint: {
type: String,
@@ -94,26 +97,40 @@ export default {
emptyState() {
if (this.issuables.length) {
return {}; // Empty state shouldn't be shown here
- } else if (this.hasFilters) {
+ }
+
+ if (this.isServiceDesk) {
+ return emptyStateHelper(this.emptyStateMeta);
+ }
+
+ if (this.hasFilters) {
return {
title: __('Sorry, your filter produced no results'),
+ svgPath: this.emptyStateMeta.svgPath,
description: __('To widen your search, change or remove filters above'),
+ primaryLink: this.emptyStateMeta.createIssuePath,
+ primaryText: __('New issue'),
};
- } else if (this.filters.state === 'opened') {
+ }
+
+ if (this.filters.state === 'opened') {
return {
title: __('There are no open issues'),
+ svgPath: this.emptyStateMeta.svgPath,
description: __('To keep this project going, create a new issue'),
- primaryLink: this.createIssuePath,
+ primaryLink: this.emptyStateMeta.createIssuePath,
primaryText: __('New issue'),
};
} else if (this.filters.state === 'closed') {
return {
title: __('There are no closed issues'),
+ svgPath: this.emptyStateMeta.svgPath,
};
}
return {
title: __('There are no issues to show'),
+ svgPath: this.emptyStateMeta.svgPath,
description: __(
'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.',
),
@@ -155,6 +172,9 @@ export default {
nextPage: this.paginationNext,
};
},
+ isServiceDesk() {
+ return this.type === 'service_desk';
+ },
isJira() {
return this.type === 'jira';
},
@@ -356,7 +376,13 @@ export default {
</ul>
<div v-else-if="issuables.length">
<div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light">
- <input type="checkbox" :checked="allIssuablesSelected" class="mr-2" @click="onSelectAll" />
+ <input
+ id="check-all-issues"
+ type="checkbox"
+ :checked="allIssuablesSelected"
+ class="mr-2"
+ @click="onSelectAll"
+ />
<strong>{{ __('Select all') }}</strong>
</div>
<ul
@@ -386,10 +412,13 @@ export default {
<gl-empty-state
v-else
:title="emptyState.title"
- :description="emptyState.description"
- :svg-path="emptySvgPath"
+ :svg-path="emptyState.svgPath"
:primary-button-link="emptyState.primaryLink"
:primary-button-text="emptyState.primaryText"
- />
+ >
+ <template #description>
+ <div v-safe-html="emptyState.description"></div>
+ </template>
+ </gl-empty-state>
</div>
</template>
diff --git a/app/assets/javascripts/issuables_list/index.js b/app/assets/javascripts/issuables_list/index.js
index 40252c10d5f..fa23d6c0eed 100644
--- a/app/assets/javascripts/issuables_list/index.js
+++ b/app/assets/javascripts/issuables_list/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import IssuableListRootApp from './components/issuable_list_root_app.vue';
import IssuablesListApp from './components/issuables_list_app.vue';
@@ -41,7 +41,7 @@ function mountIssuablesListApp() {
}
document.querySelectorAll('.js-issuables-list').forEach(el => {
- const { canBulkEdit, ...data } = el.dataset;
+ const { canBulkEdit, emptyStateMeta = {}, ...data } = el.dataset;
return new Vue({
el,
@@ -49,6 +49,10 @@ function mountIssuablesListApp() {
return createElement(IssuablesListApp, {
props: {
...data,
+ emptyStateMeta:
+ Object.keys(emptyStateMeta).length !== 0
+ ? convertObjectPropsToCamelCase(JSON.parse(emptyStateMeta))
+ : {},
canBulkEdit: Boolean(canBulkEdit),
},
});
diff --git a/app/assets/javascripts/issuables_list/service_desk_helper.js b/app/assets/javascripts/issuables_list/service_desk_helper.js
new file mode 100644
index 00000000000..4b4a38c2205
--- /dev/null
+++ b/app/assets/javascripts/issuables_list/service_desk_helper.js
@@ -0,0 +1,55 @@
+import { __ } from '~/locale';
+
+/**
+ * Returns the attributes used for gl-empty-state in the Service Desk issues list.
+ */
+// eslint-disable-next-line import/prefer-default-export
+export function emptyStateHelper(emptyStateMeta) {
+ const { isServiceDeskSupported, svgPath, serviceDeskHelpPage } = emptyStateMeta;
+
+ if (isServiceDeskSupported) {
+ const title = __(
+ 'Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab',
+ );
+ const commonMessage = __(
+ 'Those emails automatically become issues (with the comments becoming the email conversation) listed here.',
+ );
+ const commonDescription = `
+ <span>${commonMessage}</span>
+ <a href="${serviceDeskHelpPage}">${__('Read more')}</a>`;
+
+ if (emptyStateMeta.canEditProjectSettings && emptyStateMeta.isServiceDeskEnabled) {
+ return {
+ title,
+ svgPath,
+ description: `<p>${__('Have your users email')} <code>${
+ emptyStateMeta.serviceDeskAddress
+ }</code></p> ${commonDescription}`,
+ };
+ }
+
+ if (emptyStateMeta.canEditProjectSettings && !emptyStateMeta.isServiceDeskEnabled) {
+ return {
+ title,
+ svgPath,
+ description: commonDescription,
+ primaryLink: emptyStateMeta.editProjectPage,
+ primaryText: __('Turn on Service Desk'),
+ };
+ }
+
+ return {
+ title,
+ svgPath,
+ description: commonDescription,
+ };
+ }
+
+ return {
+ title: __('Service Desk is enabled but not yet active'),
+ svgPath,
+ description: __('You must set up incoming email before it becomes active.'),
+ primaryLink: emptyStateMeta.incomingEmailHelpPage,
+ primaryText: __('More information'),
+ };
+}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index a01faeb1c8d..f1b37525a6d 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index bcf5dc2aaaf..992d87a969f 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -2,7 +2,7 @@
import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import { __, s__, sprintf } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import Poll from '~/lib/utils/poll';
import eventHub from '../event_hub';
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index f2462e50093..abb63f606ae 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -1,7 +1,7 @@
<script>
import $ from 'jquery';
import { s__, sprintf } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor';
diff --git a/app/assets/javascripts/issue_show/components/issuable_header_warnings.vue b/app/assets/javascripts/issue_show/components/issuable_header_warnings.vue
deleted file mode 100644
index b6816be9eb8..00000000000
--- a/app/assets/javascripts/issue_show/components/issuable_header_warnings.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<script>
-import { mapState } from 'vuex';
-import Icon from '~/vue_shared/components/icon.vue';
-
-export default {
- components: {
- Icon,
- },
- computed: {
- ...mapState({
- confidential: ({ noteableData }) => noteableData.confidential,
- dicussionLocked: ({ noteableData }) => noteableData.discussion_locked,
- }),
- },
-};
-</script>
-
-<template>
- <div class="gl-display-inline-block">
- <div v-if="confidential" class="issuable-warning-icon inline">
- <icon class="icon" name="eye-slash" data-testid="confidential" />
- </div>
-
- <div v-if="dicussionLocked" class="issuable-warning-icon inline">
- <icon class="icon" name="lock" data-testid="locked" />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index fe4ff133145..e170d338408 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,8 +1,6 @@
import Vue from 'vue';
import issuableApp from './components/app.vue';
-import IssuableHeaderWarnings from './components/issuable_header_warnings.vue';
import { parseIssuableData } from './utils/parse_data';
-import { store } from '~/notes/stores';
export default function initIssueableApp() {
return new Vue({
@@ -17,13 +15,3 @@ export default function initIssueableApp() {
},
});
}
-
-export function issuableHeaderWarnings() {
- return new Vue({
- el: document.getElementById('js-issuable-header-warnings'),
- store,
- render(createElement) {
- return createElement(IssuableHeaderWarnings);
- },
- });
-}
diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js
index 05e384adad3..8cd1c1b0e56 100644
--- a/app/assets/javascripts/issue_show/utils/parse_data.js
+++ b/app/assets/javascripts/issue_show/utils/parse_data.js
@@ -1,4 +1,4 @@
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
export const parseIssuableData = () => {
try {
diff --git a/app/assets/javascripts/jira_import/components/jira_import_app.vue b/app/assets/javascripts/jira_import/components/jira_import_app.vue
index 60ddacd49dd..0f690d17da9 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_app.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_app.vue
@@ -1,11 +1,7 @@
<script>
-import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { last } from 'lodash';
-import { __ } from '~/locale';
import getJiraImportDetailsQuery from '../queries/get_jira_import_details.query.graphql';
-import getJiraUserMappingMutation from '../queries/get_jira_user_mapping.mutation.graphql';
-import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql';
-import { addInProgressImportToStore } from '../utils/cache_update';
import { isInProgress, extractJiraProjectsOptions } from '../utils/jira_import_utils';
import JiraImportForm from './jira_import_form.vue';
import JiraImportProgress from './jira_import_progress.vue';
@@ -16,7 +12,6 @@ export default {
components: {
GlAlert,
GlLoadingIcon,
- GlSprintf,
JiraImportForm,
JiraImportProgress,
JiraImportSetup,
@@ -53,10 +48,7 @@ export default {
},
data() {
return {
- isSubmitting: false,
jiraImportDetails: {},
- selectedProject: undefined,
- userMappings: [],
errorMessage: '',
showAlert: false,
};
@@ -80,86 +72,7 @@ export default {
},
},
},
- computed: {
- numberOfPreviousImports() {
- return this.jiraImportDetails.imports?.reduce?.(
- (acc, jiraProject) => (jiraProject.jiraProjectKey === this.selectedProject ? acc + 1 : acc),
- 0,
- );
- },
- hasPreviousImports() {
- return this.numberOfPreviousImports > 0;
- },
- importLabel() {
- return this.selectedProject
- ? `jira-import::${this.selectedProject}-${this.numberOfPreviousImports + 1}`
- : 'jira-import::KEY-1';
- },
- },
- mounted() {
- if (this.isJiraConfigured) {
- this.$apollo
- .mutate({
- mutation: getJiraUserMappingMutation,
- variables: {
- input: {
- projectPath: this.projectPath,
- },
- },
- })
- .then(({ data }) => {
- if (data.jiraImportUsers.errors.length) {
- this.setAlertMessage(data.jiraImportUsers.errors.join('. '));
- } else {
- this.userMappings = data.jiraImportUsers.jiraUsers;
- }
- })
- .catch(() => this.setAlertMessage(__('There was an error retrieving the Jira users.')));
- }
- },
methods: {
- initiateJiraImport(project) {
- this.isSubmitting = true;
-
- this.$apollo
- .mutate({
- mutation: initiateJiraImportMutation,
- variables: {
- input: {
- jiraProjectKey: project,
- projectPath: this.projectPath,
- usersMapping: this.userMappings.map(({ gitlabId, jiraAccountId }) => ({
- gitlabId,
- jiraAccountId,
- })),
- },
- },
- update: (store, { data }) =>
- addInProgressImportToStore(store, data.jiraImportStart, this.projectPath),
- })
- .then(({ data }) => {
- if (data.jiraImportStart.errors.length) {
- this.setAlertMessage(data.jiraImportStart.errors.join('. '));
- } else {
- this.selectedProject = undefined;
- }
- })
- .catch(() => this.setAlertMessage(__('There was an error importing the Jira project.')))
- .finally(() => {
- this.isSubmitting = false;
- });
- },
- updateMapping(jiraAccountId, gitlabId, gitlabUsername) {
- this.userMappings = this.userMappings.map(userMapping =>
- userMapping.jiraAccountId === jiraAccountId
- ? {
- ...userMapping,
- gitlabId,
- gitlabUsername,
- }
- : userMapping,
- );
- },
setAlertMessage(message) {
this.errorMessage = message;
this.showAlert = true;
@@ -168,9 +81,6 @@ export default {
this.showAlert = false;
},
},
- previousImportsMessage: __(
- 'You have imported from this project %{numberOfPreviousImports} times before. Each new import will create duplicate issues.',
- ),
};
</script>
@@ -179,11 +89,6 @@ export default {
<gl-alert v-if="showAlert" variant="danger" @dismiss="dismissAlert">
{{ errorMessage }}
</gl-alert>
- <gl-alert v-if="hasPreviousImports" variant="warning" :dismissible="false">
- <gl-sprintf :message="$options.previousImportsMessage">
- <template #numberOfPreviousImports>{{ numberOfPreviousImports }}</template>
- </gl-sprintf>
- </gl-alert>
<jira-import-setup
v-if="!isJiraConfigured"
@@ -201,15 +106,12 @@ export default {
/>
<jira-import-form
v-else
- v-model="selectedProject"
- :import-label="importLabel"
- :is-submitting="isSubmitting"
:issues-path="issuesPath"
+ :jira-imports="jiraImportDetails.imports"
:jira-projects="jiraImportDetails.projects"
:project-id="projectId"
- :user-mappings="userMappings"
- @initiateJiraImport="initiateJiraImport"
- @updateMapping="updateMapping"
+ :project-path="projectPath"
+ @error="setAlertMessage"
/>
</div>
</template>
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index 24bfb49a7d1..b5d17398f3a 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlAlert,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
@@ -10,15 +11,27 @@ import {
GlLabel,
GlLoadingIcon,
GlSearchBoxByType,
+ GlSprintf,
GlTable,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
+import getJiraUserMappingMutation from '../queries/get_jira_user_mapping.mutation.graphql';
+import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql';
+import { addInProgressImportToStore } from '../utils/cache_update';
+import {
+ debounceWait,
+ dropdownLabel,
+ previousImportsMessage,
+ tableConfig,
+ userMappingMessage,
+} from '../utils/constants';
export default {
name: 'JiraImportForm',
components: {
+ GlAlert,
GlButton,
GlNewDropdown,
GlNewDropdownItem,
@@ -29,35 +42,21 @@ export default {
GlLabel,
GlLoadingIcon,
GlSearchBoxByType,
+ GlSprintf,
GlTable,
},
currentUsername: gon.current_username,
- dropdownLabel: __('The GitLab user to which the Jira user %{jiraDisplayName} will be mapped'),
- tableConfig: [
- {
- key: 'jiraDisplayName',
- label: __('Jira display name'),
- },
- {
- key: 'arrow',
- label: '',
- },
- {
- key: 'gitlabUsername',
- label: __('GitLab username'),
- },
- ],
+ dropdownLabel,
+ previousImportsMessage,
+ tableConfig,
+ userMappingMessage,
props: {
- importLabel: {
+ issuesPath: {
type: String,
required: true,
},
- isSubmitting: {
- type: Boolean,
- required: true,
- },
- issuesPath: {
- type: String,
+ jiraImports: {
+ type: Array,
required: true,
},
jiraProjects: {
@@ -68,21 +67,19 @@ export default {
type: String,
required: true,
},
- userMappings: {
- type: Array,
- required: true,
- },
- value: {
+ projectPath: {
type: String,
- required: false,
- default: undefined,
+ required: true,
},
},
data() {
return {
isFetching: false,
+ isSubmitting: false,
searchTerm: '',
+ selectedProject: undefined,
selectState: null,
+ userMappings: [],
users: [],
};
},
@@ -90,13 +87,45 @@ export default {
shouldShowNoMatchesFoundText() {
return !this.isFetching && this.users.length === 0;
},
+ numberOfPreviousImports() {
+ return this.jiraImports?.reduce?.(
+ (acc, jiraProject) => (jiraProject.jiraProjectKey === this.selectedProject ? acc + 1 : acc),
+ 0,
+ );
+ },
+ hasPreviousImports() {
+ return this.numberOfPreviousImports > 0;
+ },
+ importLabel() {
+ return this.selectedProject
+ ? `jira-import::${this.selectedProject}-${this.numberOfPreviousImports + 1}`
+ : 'jira-import::KEY-1';
+ },
},
watch: {
searchTerm: debounce(function debouncedUserSearch() {
this.searchUsers();
- }, 500),
+ }, debounceWait),
},
mounted() {
+ this.$apollo
+ .mutate({
+ mutation: getJiraUserMappingMutation,
+ variables: {
+ input: {
+ projectPath: this.projectPath,
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.jiraImportUsers.errors.length) {
+ this.$emit('error', data.jiraImportUsers.errors.join('. '));
+ } else {
+ this.userMappings = data.jiraImportUsers.jiraUsers;
+ }
+ })
+ .catch(() => this.$emit('error', __('There was an error retrieving the Jira users.')));
+
this.searchUsers()
.then(data => {
this.initialUsers = data;
@@ -129,13 +158,54 @@ export default {
},
initiateJiraImport(event) {
event.preventDefault();
- if (this.value) {
+
+ if (this.selectedProject) {
this.hideValidationError();
- this.$emit('initiateJiraImport', this.value);
+
+ this.isSubmitting = true;
+
+ this.$apollo
+ .mutate({
+ mutation: initiateJiraImportMutation,
+ variables: {
+ input: {
+ jiraProjectKey: this.selectedProject,
+ projectPath: this.projectPath,
+ usersMapping: this.userMappings.map(({ gitlabId, jiraAccountId }) => ({
+ gitlabId,
+ jiraAccountId,
+ })),
+ },
+ },
+ update: (store, { data }) =>
+ addInProgressImportToStore(store, data.jiraImportStart, this.projectPath),
+ })
+ .then(({ data }) => {
+ if (data.jiraImportStart.errors.length) {
+ this.$emit('error', data.jiraImportStart.errors.join('. '));
+ } else {
+ this.selectedProject = undefined;
+ }
+ })
+ .catch(() => this.$emit('error', __('There was an error importing the Jira project.')))
+ .finally(() => {
+ this.isSubmitting = false;
+ });
} else {
this.showValidationError();
}
},
+ updateMapping(jiraAccountId, gitlabId, gitlabUsername) {
+ this.userMappings = this.userMappings.map(userMapping =>
+ userMapping.jiraAccountId === jiraAccountId
+ ? {
+ ...userMapping,
+ gitlabId,
+ gitlabUsername,
+ }
+ : userMapping,
+ );
+ },
hideValidationError() {
this.selectState = null;
},
@@ -148,8 +218,16 @@ export default {
<template>
<div>
+ <gl-alert v-if="hasPreviousImports" variant="warning" :dismissible="false">
+ <gl-sprintf :message="$options.previousImportsMessage">
+ <template #numberOfPreviousImports>{{ numberOfPreviousImports }}</template>
+ </gl-sprintf>
+ </gl-alert>
+
<h3 class="page-title">{{ __('New Jira import') }}</h3>
+
<hr />
+
<form @submit="initiateJiraImport">
<gl-form-group
class="row align-items-center"
@@ -160,12 +238,11 @@ export default {
>
<gl-form-select
id="jira-project-select"
+ v-model="selectedProject"
data-qa-selector="jira_project_dropdown"
class="mb-2"
:options="jiraProjects"
:state="selectState"
- :value="value"
- @change="$emit('input', $event)"
/>
</gl-form-group>
@@ -186,17 +263,7 @@ export default {
<h4 class="gl-mb-4">{{ __('Jira-GitLab user mapping template') }}</h4>
- <p>
- {{
- __(
- `Jira users have been matched with similar GitLab users.
- This can be overwritten by selecting a GitLab user from the dropdown in the "GitLab
- username" column.
- If it wasn't possible to match a Jira user with a GitLab user, the dropdown defaults to
- the user conducting the import.`,
- )
- }}
- </p>
+ <p>{{ $options.userMappingMessage }}</p>
<gl-table :fields="$options.tableConfig" :items="userMappings" fixed>
<template #cell(arrow)>
@@ -221,7 +288,7 @@ export default {
v-for="user in users"
v-else
:key="user.id"
- @click="$emit('updateMapping', data.item.jiraAccountId, user.id, user.username)"
+ @click="updateMapping(data.item.jiraAccountId, user.id, user.username)"
>
{{ user.username }} ({{ user.name }})
</gl-new-dropdown-item>
diff --git a/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql b/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql
index 1f7c52eec58..cca33af342c 100644
--- a/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql
+++ b/app/assets/javascripts/jira_import/queries/get_jira_user_mapping.mutation.graphql
@@ -5,6 +5,8 @@ mutation($input: JiraImportUsersInput!) {
jiraDisplayName
jiraEmail
gitlabId
+ gitlabName
+ gitlabUsername
}
errors
}
diff --git a/app/assets/javascripts/jira_import/utils/constants.js b/app/assets/javascripts/jira_import/utils/constants.js
new file mode 100644
index 00000000000..6adc3e5306c
--- /dev/null
+++ b/app/assets/javascripts/jira_import/utils/constants.js
@@ -0,0 +1,29 @@
+import { __ } from '~/locale';
+
+export const debounceWait = 500;
+
+export const dropdownLabel = __(
+ 'The GitLab user to which the Jira user %{jiraDisplayName} will be mapped',
+);
+
+export const previousImportsMessage = __(`You have imported from this project
+ %{numberOfPreviousImports} times before. Each new import will create duplicate issues.`);
+
+export const tableConfig = [
+ {
+ key: 'jiraDisplayName',
+ label: __('Jira display name'),
+ },
+ {
+ key: 'arrow',
+ label: '',
+ },
+ {
+ key: 'gitlabUsername',
+ label: __('GitLab username'),
+ },
+];
+
+export const userMappingMessage = __(`Jira users have been imported from the configured Jira
+ instance. They can be mapped by selecting a GitLab user from the dropdown in the "GitLab username"
+ column. When the form appears, the dropdown defaults to the user conducting the import.`);
diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
index b2f9bf2a348..6183779acd4 100644
--- a/app/assets/javascripts/jobs/components/artifacts_block.vue
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -48,7 +48,7 @@ export default {
)
}}</span>
</p>
- <div class="btn-group d-flex prepend-top-10" role="group">
+ <div class="btn-group d-flex gl-mt-3" role="group">
<gl-link
v-if="artifact.keep_path"
:href="artifact.keep_path"
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index e2bc413e3ce..0ee8cd6c5ad 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -71,9 +71,9 @@ export default {
<div class="col-12">
<div class="text-content">
- <h4 class="js-job-empty-state-title text-center">{{ title }}</h4>
+ <h4 class="text-center" data-testid="job-empty-state-title">{{ title }}</h4>
- <p v-if="content" class="js-job-empty-state-content">{{ content }}</p>
+ <p v-if="content" data-testid="job-empty-state-content">{{ content }}</p>
</div>
<manual-variables-form
v-if="shouldRenderManualVariables"
@@ -85,7 +85,8 @@ export default {
<gl-link
:href="action.path"
:data-method="action.method"
- class="js-job-empty-state-action btn btn-primary"
+ class="btn btn-primary"
+ data-testid="job-empty-state-action"
>{{ action.button_title }}</gl-link
>
</div>
diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue
index 9166c13a4fb..ec7868d9235 100644
--- a/app/assets/javascripts/jobs/components/environments_block.vue
+++ b/app/assets/javascripts/jobs/components/environments_block.vue
@@ -1,8 +1,8 @@
<script>
import { isEmpty } from 'lodash';
+import { GlSprintf, GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { __ } from '../../locale';
-import { GlSprintf, GlLink } from '@gitlab/ui';
export default {
creatingEnvironment: 'creating',
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index f43a058b5f8..e760706c97e 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -198,17 +198,13 @@ export default {
</script>
<template>
<div>
- <gl-loading-icon
- v-if="isLoading"
- size="lg"
- class="js-job-loading qa-loading-animation prepend-top-20"
- />
+ <gl-loading-icon v-if="isLoading" size="lg" class="qa-loading-animation prepend-top-20" />
<template v-else-if="shouldRenderContent">
- <div class="js-job-content build-page">
+ <div class="build-page" data-testid="job-content">
<!-- Header Section -->
<header>
- <div class="js-build-header build-header top-area">
+ <div class="build-header top-area">
<ci-header
:status="job.status"
:item-id="job.id"
@@ -230,7 +226,6 @@ export default {
<!-- Body Section -->
<stuck-block
v-if="job.stuck"
- class="js-job-stuck"
:has-no-runners-for-project="hasRunnersForProject"
:tags="job.tags"
:runners-path="runnerSettingsUrl"
@@ -238,13 +233,11 @@ export default {
<unmet-prerequisites-block
v-if="hasUnmetPrerequisitesFailure"
- class="js-job-failed"
:help-path="deploymentHelpUrl"
/>
<shared-runner
v-if="shouldRenderSharedRunnerLimitWarning"
- class="js-shared-runner-limit"
:quota-used="job.runners.quota.used"
:quota-limit="job.runners.quota.limit"
:runners-path="runnerHelpUrl"
@@ -254,7 +247,6 @@ export default {
<environments-block
v-if="hasEnvironment"
- class="js-job-environment"
:deployment-status="job.deployment_status"
:deployment-cluster="job.deployment_cluster"
:icon-status="job.status"
@@ -262,7 +254,7 @@ export default {
<erased-block
v-if="job.erased_at"
- class="js-job-erased-block"
+ data-testid="job-erased-block"
:user="job.erased_by"
:erased-at="job.erased_at"
/>
@@ -270,8 +262,9 @@ export default {
<div
v-if="job.archived"
ref="sticky"
- class="js-archived-job gl-mt-3 archived-job"
+ class="gl-mt-3 archived-job"
:class="{ 'sticky-top border-bottom-0': hasTrace }"
+ data-testid="archived-job"
>
<icon name="lock" class="align-text-bottom" />
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
@@ -305,7 +298,6 @@ export default {
<!-- empty state -->
<empty-state
v-if="!hasTrace"
- class="js-job-empty-state"
:illustration-path="emptyStateIllustration.image"
:illustration-size-class="emptyStateIllustration.size"
:title="emptyStateTitle"
@@ -323,12 +315,12 @@ export default {
<sidebar
v-if="shouldRenderContent"
- class="js-job-sidebar"
:class="{
'right-sidebar-expanded': isSidebarOpen,
'right-sidebar-collapsed': !isSidebarOpen,
}"
:runner-help-url="runnerHelpUrl"
+ data-testid="job-sidebar"
/>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index a68174d8e1d..4d314eaa106 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -71,13 +71,14 @@ export default {
<template>
<div class="top-bar">
<!-- truncate information -->
- <div class="js-truncated-info truncated-info d-none d-sm-block float-left">
+ <div class="truncated-info d-none d-sm-block float-left" data-testid="log-truncated-info">
<template v-if="isTraceSizeVisible">
{{ jobLogSize }}
<gl-link
v-if="rawPath"
:href="rawPath"
- class="js-raw-link text-plain text-underline gl-ml-2"
+ class="text-plain text-underline gl-ml-2"
+ data-testid="raw-link"
>{{ s__('Job|Complete Raw') }}</gl-link
>
</template>
@@ -91,7 +92,8 @@ export default {
v-gl-tooltip.body
:title="s__('Job|Show complete raw')"
:href="rawPath"
- class="js-raw-link-controller controllers-buttons"
+ class="controllers-buttons"
+ data-testid="job-raw-link-controller"
>
<icon name="doc-text" />
</gl-link>
@@ -102,7 +104,8 @@ export default {
:title="s__('Job|Erase job log')"
:href="erasePath"
:data-confirm="__('Are you sure you want to erase this build?')"
- class="js-erase-link controllers-buttons"
+ class="controllers-buttons"
+ data-testid="job-log-erase-link"
data-method="post"
>
<icon name="remove" />
@@ -114,7 +117,8 @@ export default {
<gl-deprecated-button
:disabled="isScrollTopDisabled"
type="button"
- class="js-scroll-top btn-scroll btn-transparent btn-blank"
+ class="btn-scroll btn-transparent btn-blank"
+ data-testid="job-controller-scroll-top"
@click="handleScrollToTop"
>
<icon name="scroll_up" />
@@ -126,6 +130,7 @@ export default {
:disabled="isScrollBottomDisabled"
class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
:class="{ animate: isScrollingDown }"
+ data-testid="job-controller-scroll-bottom"
@click="handleScrollToBottom"
v-html="$options.scrollDown"
/>
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
index d83c598dd48..9236624a191 100644
--- a/app/assets/javascripts/jobs/components/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -100,7 +100,7 @@ export default {
};
</script>
<template>
- <div class="js-manual-vars-form col-12">
+ <div class="col-12" data-testid="manual-vars-form">
<label>{{ s__('CiVariables|Variables') }}</label>
<div class="ci-table">
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index 83ba528cfa2..517da16dcf8 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -147,7 +147,8 @@ export default {
<gl-link
v-if="job.new_issue_path"
:href="job.new_issue_path"
- class="js-new-issue btn btn-success btn-inverted float-left mr-2"
+ class="btn btn-success btn-inverted float-left mr-2"
+ data-testid="job-new-issue"
>{{ __('New issue') }}</gl-link
>
<gl-link
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index b69e6f9686f..8e8202246a2 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -1,10 +1,13 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlAlert, GlBadge, GlLink } from '@gitlab/ui';
+import { s__ } from '../../locale';
/**
* Renders Stuck Runners block for job's view.
*/
export default {
components: {
+ GlAlert,
+ GlBadge,
GlLink,
},
props: {
@@ -22,35 +25,50 @@ export default {
required: true,
},
},
+ computed: {
+ hasNoRunnersWithCorrespondingTags() {
+ return this.tags.length > 0;
+ },
+ stuckData() {
+ if (this.hasNoRunnersWithCorrespondingTags) {
+ return {
+ text: s__(`Job|This job is stuck because you don't have
+ any active runners online or available with any of these tags assigned to them:`),
+ dataTestId: 'job-stuck-with-tags',
+ showTags: true,
+ };
+ } else if (this.hasNoRunnersForProject) {
+ return {
+ text: s__(`Job|This job is stuck because the project
+ doesn't have any runners online assigned to it.`),
+ dataTestId: 'job-stuck-no-runners',
+ showTags: false,
+ };
+ }
+
+ return {
+ text: s__(`Job|This job is stuck because you don't
+ have any active runners that can run this job.`),
+ dataTestId: 'job-stuck-no-active-runners',
+ showTags: false,
+ };
+ },
+ },
};
</script>
<template>
- <div class="bs-callout bs-callout-warning">
- <p v-if="tags.length" class="js-stuck-with-tags gl-mb-0">
- {{
- s__(`This job is stuck because you don't have
- any active runners online or available with any of these tags assigned to them:`)
- }}
- <span v-for="(tag, index) in tags" :key="index" class="badge badge-primary gl-mr-2">
- {{ tag }}
- </span>
+ <gl-alert variant="warning" :dismissible="false">
+ <p class="gl-mb-0" :data-testid="stuckData.dataTestId">
+ {{ stuckData.text }}
+ <template v-if="stuckData.showTags">
+ <gl-badge v-for="tag in tags" :key="tag" variant="info">
+ {{ tag }}
+ </gl-badge>
+ </template>
</p>
- <p v-else-if="hasNoRunnersForProject" class="js-stuck-no-runners gl-mb-0">
- {{
- s__(`Job|This job is stuck because the project
- doesn't have any runners online assigned to it.`)
- }}
- </p>
- <p v-else class="js-stuck-no-active-runner gl-mb-0">
- {{
- s__(`This job is stuck because you don't
- have any active runners that can run this job.`)
- }}
- </p>
-
{{ __('Go to project') }}
- <gl-link v-if="runnersPath" :href="runnersPath" class="js-runners-path">
+ <gl-link v-if="runnersPath" :href="runnersPath">
{{ __('CI settings') }}
</gl-link>
- </div>
+ </gl-alert>
</template>
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index 4bd8d6f58a6..1e4b5e986db 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -3,7 +3,7 @@ import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { setFaviconOverlay, resetFavicon } from '~/lib/utils/common_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import {
canScroll,
@@ -247,6 +247,3 @@ export const triggerManualJob = ({ state }, variables) => {
})
.catch(() => flash(__('An error occurred while triggering the job.')));
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 3f02f924eed..dc4a3578a86 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -46,6 +46,3 @@ export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceCo
export const hasRunnersForProject = state =>
state.job.runners.available && !state.job.runners.online;
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 5dcc719f7c3..4922166acd0 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 63c4ad3c410..1fb8e270e0e 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -3,12 +3,12 @@
/* global ListLabel */
import $ from 'jquery';
-import { difference, isEqual, escape, sortBy, template } from 'lodash';
+import { difference, isEqual, escape, sortBy, template, union } from 'lodash';
import { sprintf, s__, __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import CreateLabelDropdown from './create_label';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import ModalStore from './boards/stores/modal_store';
import boardsStore from './boards/stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
@@ -477,13 +477,11 @@ export default class LabelsSelect {
const linkOpenTag =
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>" class="gl-link gl-label-link has-tooltip" <%= linkAttrs %> title="<%= tooltipTitleTemplate({ label, isScopedLabel, enableScopedLabels, escapeStr }) %>">';
- const spanOpenTag =
- '<span class="gl-label-text" style="background-color: <%= escapeStr(label.color) %>; color: <%= escapeStr(label.text_color) %>;">';
const labelTemplate = template(
[
'<span class="gl-label">',
linkOpenTag,
- spanOpenTag,
+ '<span class="gl-label-text <%= labelTextClass({ label, escapeStr }) %>" style="background-color: <%= escapeStr(label.color) %>;">',
'<%- label.title %>',
'</span>',
'</a>',
@@ -491,18 +489,24 @@ export default class LabelsSelect {
].join(''),
);
- const rightLabelTextColor = ({ label, escapeStr }) => {
- return escapeStr(label.text_color === '#FFFFFF' ? label.color : label.text_color);
+ const labelTextClass = ({ label, escapeStr }) => {
+ return escapeStr(
+ label.text_color === '#FFFFFF' ? 'gl-label-text-light' : 'gl-label-text-dark',
+ );
+ };
+
+ const rightLabelTextClass = ({ label, escapeStr }) => {
+ return escapeStr(label.text_color === '#333333' ? labelTextClass({ label, escapeStr }) : '');
};
const scopedLabelTemplate = template(
[
'<span class="gl-label gl-label-scoped" style="color: <%= escapeStr(label.color) %>; --label-inset-border: inset 0 0 0 2px <%= escapeStr(label.color) %>;">',
linkOpenTag,
- spanOpenTag,
+ '<span class="gl-label-text <%= labelTextClass({ label, escapeStr }) %>" style="background-color: <%= escapeStr(label.color) %>;">',
'<%- label.title.slice(0, label.title.lastIndexOf("::")) %>',
'</span>',
- '<span class="gl-label-text" style="color: <%= rightLabelTextColor({ label, escapeStr }) %>;">',
+ '<span class="gl-label-text <%= rightLabelTextClass({ label, escapeStr }) %>">',
'<%- label.title.slice(label.title.lastIndexOf("::") + 2) %>',
'</span>',
'</a>',
@@ -526,9 +530,9 @@ export default class LabelsSelect {
[
'<% labels.forEach(function(label){ %>',
'<% if (isScopedLabel(label) && enableScopedLabels) { %>',
- '<%= scopedLabelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, rightLabelTextColor, tooltipTitleTemplate, escapeStr, linkAttrs: \'data-html="true"\' }) %>',
+ '<%= scopedLabelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, labelTextClass, rightLabelTextClass, tooltipTitleTemplate, escapeStr, linkAttrs: \'data-html="true"\' }) %>',
'<% } else { %>',
- '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, escapeStr, linkAttrs: "" }) %>',
+ '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, labelTextClass, tooltipTitleTemplate, escapeStr, linkAttrs: "" }) %>',
'<% } %>',
'<% }); %>',
].join(''),
@@ -537,7 +541,8 @@ export default class LabelsSelect {
return tpl({
...tplData,
labelTemplate,
- rightLabelTextColor,
+ labelTextClass,
+ rightLabelTextClass,
scopedLabelTemplate,
tooltipTitleTemplate,
isScopedLabel,
@@ -560,15 +565,15 @@ export default class LabelsSelect {
IssuableBulkUpdateActions.willUpdateLabels = true;
}
// eslint-disable-next-line class-methods-use-this
- setDropdownData($dropdown, isChecking, labelId) {
+ setDropdownData($dropdown, isMarking, labelId) {
let userCheckedIds = $dropdown.data('user-checked') || [];
let userUncheckedIds = $dropdown.data('user-unchecked') || [];
- if (isChecking) {
- userCheckedIds = userCheckedIds.concat(labelId);
+ if (isMarking) {
+ userCheckedIds = union(userCheckedIds, [labelId]);
userUncheckedIds = difference(userUncheckedIds, [labelId]);
} else {
- userUncheckedIds = userUncheckedIds.concat(labelId);
+ userUncheckedIds = union(userUncheckedIds, [labelId]);
userCheckedIds = difference(userCheckedIds, [labelId]);
}
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 4314e5e1afb..0ecf3301250 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
+import initWhatsNew from '~/whats_new';
function hideEndFade($scrollingTabs) {
$scrollingTabs.each(function scrollTabsLoop() {
@@ -20,6 +21,7 @@ export default function initLayoutNav() {
contextualSidebar.bindEvents();
initFlyOutNav();
+ initWhatsNew();
// We need to init it on DomContentLoaded as others could also call it
$(document).on('init.scrolling-tabs', () => {
diff --git a/app/assets/javascripts/lib/chrome_84_icon_fix.js b/app/assets/javascripts/lib/chrome_84_icon_fix.js
new file mode 100644
index 00000000000..60497186c19
--- /dev/null
+++ b/app/assets/javascripts/lib/chrome_84_icon_fix.js
@@ -0,0 +1,78 @@
+import { debounce } from 'lodash';
+
+/*
+ Chrome and Edge 84 have a bug relating to icon sprite svgs
+ https://bugs.chromium.org/p/chromium/issues/detail?id=1107442
+
+ If the SVG is loaded, under certain circumstances the icons are not
+ shown. We load our sprite icons with JS and add them to the body.
+ Then we iterate over all the `use` elements and replace their reference
+ to that svg which we added internally. In order to avoid id conflicts,
+ those are renamed with a unique prefix.
+
+ We do that once the DOMContentLoaded fired and otherwise we use a
+ mutation observer to re-trigger this logic.
+
+ In order to not have a big impact on performance or to cause flickering
+ of of content,
+
+ 1. we only do it for each svg once
+ 2. we debounce the event handler and just do it in a requestIdleCallback
+
+ Before we tried to do it with the library svg4everybody and it had a big
+ performance impact. See:
+ https://gitlab.com/gitlab-org/quality/performance/-/issues/312
+ */
+document.addEventListener('DOMContentLoaded', async () => {
+ const GITLAB_SVG_PREFIX = 'chrome-issue-230433-gitlab-svgs';
+ const FILE_ICON_PREFIX = 'chrome-issue-230433-file-icons';
+ const SKIP_ATTRIBUTE = 'data-replaced-by-chrome-issue-230433';
+
+ const fixSVGs = () => {
+ requestIdleCallback(() => {
+ document.querySelectorAll(`use:not([${SKIP_ATTRIBUTE}])`).forEach(use => {
+ const href = use?.getAttribute('href') ?? use?.getAttribute('xlink:href') ?? '';
+
+ if (href.includes(window.gon.sprite_icons)) {
+ use.removeAttribute('xlink:href');
+ use.setAttribute('href', `#${GITLAB_SVG_PREFIX}-${href.split('#')[1]}`);
+ } else if (href.includes(window.gon.sprite_file_icons)) {
+ use.removeAttribute('xlink:href');
+ use.setAttribute('href', `#${FILE_ICON_PREFIX}-${href.split('#')[1]}`);
+ }
+
+ use.setAttribute(SKIP_ATTRIBUTE, 'true');
+ });
+ });
+ };
+
+ const watchForNewSVGs = () => {
+ const observer = new MutationObserver(debounce(fixSVGs, 200));
+ observer.observe(document.querySelector('body'), {
+ childList: true,
+ attributes: false,
+ subtree: true,
+ });
+ };
+
+ const retrieveIconSprites = async (url, prefix) => {
+ const div = document.createElement('div');
+ div.classList.add('hidden');
+ const result = await fetch(url);
+ div.innerHTML = await result.text();
+ div.querySelectorAll('[id]').forEach(node => {
+ node.setAttribute('id', `${prefix}-${node.getAttribute('id')}`);
+ });
+ document.body.append(div);
+ };
+
+ if (window.gon && window.gon.sprite_icons) {
+ await retrieveIconSprites(window.gon.sprite_icons, GITLAB_SVG_PREFIX);
+ if (window.gon.sprite_file_icons) {
+ await retrieveIconSprites(window.gon.sprite_file_icons, FILE_ICON_PREFIX);
+ }
+
+ fixSVGs();
+ watchForNewSVGs();
+ }
+});
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index b6c41ffa7ab..4fed121779e 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -14,7 +14,7 @@ export const fetchPolicies = {
};
export default (resolvers = {}, config = {}) => {
- let uri = `${gon.relative_url_root}/api/graphql`;
+ let uri = `${gon.relative_url_root || ''}/api/graphql`;
if (config.baseUrl) {
// Prepend baseUrl and ensure that `///` are replaced with `/`
diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js
index cb2e8a76c08..a047cebc8ab 100644
--- a/app/assets/javascripts/lib/utils/axios_startup_calls.js
+++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js
@@ -34,14 +34,17 @@ const setupAxiosStartupCalls = axios => {
});
// eslint-disable-next-line promise/no-nesting
- return res.json().then(data => ({
- data,
- status: res.status,
- statusText: res.statusText,
- headers: fetchHeaders,
- config: req,
- request: req,
- }));
+ return res
+ .clone()
+ .json()
+ .then(data => ({
+ data,
+ status: res.status,
+ statusText: res.statusText,
+ headers: fetchHeaders,
+ config: req,
+ request: req,
+ }));
});
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8bf9a281151..bcf302cc262 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -4,12 +4,12 @@
import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
+import { isFunction } from 'lodash';
+import Cookies from 'js-cookie';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
-import { isFunction } from 'lodash';
-import Cookies from 'js-cookie';
export const getPagePath = (index = 0) => {
const page = $('body').attr('data-page') || '';
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 6e02fc1eb91..e26b63fbb85 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -689,3 +689,37 @@ export const approximateDuration = (seconds = 0) => {
}
return n__('1 day', '%d days', seconds < ONE_DAY_LIMIT ? 1 : days);
};
+
+/**
+ * A utility function which helps creating a date object
+ * for a specific date. Accepts the year, month and day
+ * returning a date object for the given params.
+ *
+ * @param {Int} year the full year as a number i.e. 2020
+ * @param {Int} month the month index i.e. January => 0
+ * @param {Int} day the day as a number i.e. 23
+ *
+ * @return {Date} the date object from the params
+ */
+export const dateFromParams = (year, month, day) => {
+ const date = new Date();
+
+ date.setFullYear(year);
+ date.setMonth(month);
+ date.setDate(day);
+
+ return date;
+};
+
+/**
+ * A utility function which computes the difference in seconds
+ * between 2 dates.
+ *
+ * @param {Date} startDate the start sate
+ * @param {Date} endDate the end date
+ *
+ * @return {Int} the difference in seconds
+ */
+export const differenceInSeconds = (startDate, endDate) => {
+ return (endDate.getTime() - startDate.getTime()) / 1000;
+};
diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js
index b1dd562f63a..32553af9af3 100644
--- a/app/assets/javascripts/lib/utils/highlight.js
+++ b/app/assets/javascripts/lib/utils/highlight.js
@@ -1,5 +1,5 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
/**
* Wraps substring matches with HTML `<span>` elements.
@@ -24,7 +24,7 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match
return string;
}
- const sanitizedValue = sanitize(string.toString(), { allowedTags: [] });
+ const sanitizedValue = sanitize(string.toString(), { ALLOWED_TAGS: [] });
// occurrences is an array of character indices that should be
// highlighted in the original string, i.e. [3, 4, 5, 7]
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index 08a77966bbd..7132986a7e6 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -22,6 +22,7 @@ const httpStatusCodes = {
CONFLICT: 409,
GONE: 410,
UNPROCESSABLE_ENTITY: 422,
+ INTERNAL_SERVER_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
};
diff --git a/app/assets/javascripts/lib/utils/keys.js b/app/assets/javascripts/lib/utils/keys.js
index 8e5420e87ea..2a8b1759e54 100644
--- a/app/assets/javascripts/lib/utils/keys.js
+++ b/app/assets/javascripts/lib/utils/keys.js
@@ -1,4 +1,2 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
export const ESC_KEY = 'Escape';
-export const ESC_KEY_IE11 = 'Esc'; // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
+export const ENTER_KEY = 'Enter';
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index a900ff34bf5..7f0c65868c2 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -46,6 +46,19 @@ import { normalizeHeaders } from './common_utils';
* 4. If HTTP response is 200, we poll.
* 5. If HTTP response is different from 200, we stop polling.
*
+ * @example
+ * // With initial delay (for, for example, reducing unnecessary requests)
+ *
+ * const poll = new Poll({
+ * resource: this.service,
+ * method: 'fetchNotes',
+ * successCallback: () => {},
+ * errorCallback: () => {},
+ * });
+ *
+ * // Performs the first request in 2.5s and then uses the `Poll-Interval` header.
+ * poll.makeDelayedRequest(2500);
+ *
*/
export default class Poll {
constructor(options = {}) {
@@ -74,6 +87,10 @@ export default class Poll {
this.options.successCallback(response);
}
+ makeDelayedRequest(delay = 0) {
+ this.timeoutID = setTimeout(() => this.makeRequest(), delay);
+ }
+
makeRequest() {
const { resource, method, data, errorCallback, notificationCallback } = this.options;
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 4d25ee9e4bd..8d23d177410 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -308,9 +308,11 @@ export function addMarkdownListeners(form) {
.off('click')
.on('click', function() {
const $this = $(this);
+ const tag = this.dataset.mdTag;
+
return updateText({
textArea: $this.closest('.md-area').find('textarea'),
- tag: $this.data('mdTag'),
+ tag,
cursorOffset: $this.data('mdCursorOffset'),
blockTag: $this.data('mdBlock'),
wrap: !$this.data('mdPrepend'),
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index c6c34b831ee..8077570158a 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -71,29 +71,56 @@ export function getParameterValues(sParam, url = window.location) {
*
* @param {Object} params - url keys and value to merge
* @param {String} url
+ * @param {Object} options
+ * @param {Boolean} options.spreadArrays - split array values into separate key/value-pairs
*/
-export function mergeUrlParams(params, url) {
+export function mergeUrlParams(params, url, options = {}) {
+ const { spreadArrays = false } = options;
const re = /^([^?#]*)(\?[^#]*)?(.*)/;
- const merged = {};
+ let merged = {};
const [, fullpath, query, fragment] = url.match(re);
if (query) {
- query
+ merged = query
.substr(1)
.split('&')
- .forEach(part => {
+ .reduce((memo, part) => {
if (part.length) {
const kv = part.split('=');
- merged[decodeUrlParameter(kv[0])] = decodeUrlParameter(kv.slice(1).join('='));
+ let key = decodeUrlParameter(kv[0]);
+ const value = decodeUrlParameter(kv.slice(1).join('='));
+ if (spreadArrays && key.endsWith('[]')) {
+ key = key.slice(0, -2);
+ if (!Array.isArray(memo[key])) {
+ return { ...memo, [key]: [value] };
+ }
+ memo[key].push(value);
+
+ return memo;
+ }
+
+ return { ...memo, [key]: value };
}
- });
+
+ return memo;
+ }, {});
}
Object.assign(merged, params);
const newQuery = Object.keys(merged)
.filter(key => merged[key] !== null)
- .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(merged[key])}`)
+ .map(key => {
+ let value = merged[key];
+ const encodedKey = encodeURIComponent(key);
+ if (spreadArrays && Array.isArray(value)) {
+ value = merged[key]
+ .map(arrayValue => encodeURIComponent(arrayValue))
+ .join(`&${encodedKey}[]=`);
+ return `${encodedKey}[]=${value}`;
+ }
+ return `${encodedKey}=${encodeURIComponent(value)}`;
+ })
.join('&');
if (newQuery) {
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index f37f48aa431..97b96cb5839 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -5,10 +5,10 @@ import {
GlSprintf,
GlIcon,
GlAlert,
- GlDropdown,
- GlDropdownHeader,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownHeader,
+ GlDeprecatedDropdownItem,
+ GlDeprecatedDropdownDivider,
GlInfiniteScroll,
} from '@gitlab/ui';
@@ -25,10 +25,10 @@ export default {
GlSprintf,
GlIcon,
GlAlert,
- GlDropdown,
- GlDropdownHeader,
- GlDropdownItem,
- GlDropdownDivider,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownHeader,
+ GlDeprecatedDropdownItem,
+ GlDeprecatedDropdownDivider,
GlInfiniteScroll,
LogSimpleFilters,
LogAdvancedFilters,
@@ -174,16 +174,16 @@ export default {
<div class="top-bar d-md-flex border bg-secondary-50 pt-2 pr-1 pb-0 pl-2">
<div class="flex-grow-0">
- <gl-dropdown
+ <gl-deprecated-dropdown
id="environments-dropdown"
:text="environments.current || managedApps.current"
:disabled="environments.isLoading"
class="mb-2 gl-h-32 pr-2 d-flex d-md-block js-environments-dropdown"
>
- <gl-dropdown-header class="gl-text-center">
+ <gl-deprecated-dropdown-header class="gl-text-center">
{{ s__('Environments|Environments') }}
- </gl-dropdown-header>
- <gl-dropdown-item
+ </gl-deprecated-dropdown-header>
+ <gl-deprecated-dropdown-item
v-for="env in environments.options"
:key="env.id"
@click="showEnvironment(env.name)"
@@ -195,12 +195,12 @@ export default {
/>
<div class="gl-flex-grow-1">{{ env.name }}</div>
</div>
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-dropdown-header class="gl-text-center">
+ </gl-deprecated-dropdown-item>
+ <gl-deprecated-dropdown-divider />
+ <gl-deprecated-dropdown-header class="gl-text-center">
{{ s__('Environments|Managed apps') }}
- </gl-dropdown-header>
- <gl-dropdown-item
+ </gl-deprecated-dropdown-header>
+ <gl-deprecated-dropdown-item
v-for="app in managedApps.options"
:key="app.id"
@click="showManagedApp(app.name)"
@@ -212,8 +212,8 @@ export default {
/>
<div class="gl-flex-grow-1">{{ app.name }}</div>
</div>
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
</div>
<log-advanced-filters
diff --git a/app/assets/javascripts/logs/components/log_simple_filters.vue b/app/assets/javascripts/logs/components/log_simple_filters.vue
index 21fe1695624..2e1270b5428 100644
--- a/app/assets/javascripts/logs/components/log_simple_filters.vue
+++ b/app/assets/javascripts/logs/components/log_simple_filters.vue
@@ -1,14 +1,19 @@
<script>
-import { s__ } from '~/locale';
import { mapActions, mapState } from 'vuex';
-import { GlIcon, GlDropdown, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui';
+import {
+ GlIcon,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownHeader,
+ GlDeprecatedDropdownItem,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
export default {
components: {
GlIcon,
- GlDropdown,
- GlDropdownHeader,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownHeader,
+ GlDeprecatedDropdownItem,
},
props: {
disabled: {
@@ -39,22 +44,22 @@ export default {
</script>
<template>
<div>
- <gl-dropdown
+ <gl-deprecated-dropdown
ref="podsDropdown"
:text="podDropdownText"
:disabled="disabled"
class="mb-2 gl-h-32 pr-2 d-flex d-md-block flex-grow-0 qa-pods-dropdown"
>
- <gl-dropdown-header class="text-center">
+ <gl-deprecated-dropdown-header class="text-center">
{{ s__('Environments|Select pod') }}
- </gl-dropdown-header>
+ </gl-deprecated-dropdown-header>
- <gl-dropdown-item v-if="!pods.options.length" disabled>
+ <gl-deprecated-dropdown-item v-if="!pods.options.length" disabled>
<span ref="noPodsMsg" class="text-muted">
{{ s__('Environments|No pods to display') }}
</span>
- </gl-dropdown-item>
- <gl-dropdown-item
+ </gl-deprecated-dropdown-item>
+ <gl-deprecated-dropdown-item
v-for="podName in pods.options"
:key="podName"
class="text-nowrap"
@@ -67,7 +72,7 @@ export default {
/>
<div class="flex-grow-1">{{ podName }}</div>
</div>
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/logs/stores/actions.js b/app/assets/javascripts/logs/stores/actions.js
index 0edd825b6e9..623516f349d 100644
--- a/app/assets/javascripts/logs/stores/actions.js
+++ b/app/assets/javascripts/logs/stores/actions.js
@@ -200,6 +200,3 @@ export const dismissRequestLogsError = ({ commit }) => {
export const dismissInvalidTimeRangeWarning = ({ commit }) => {
commit(types.HIDE_TIME_RANGE_INVALID_WARNING);
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/logs/stores/mutations.js b/app/assets/javascripts/logs/stores/mutations.js
index be22204d88d..147f562057f 100644
--- a/app/assets/javascripts/logs/stores/mutations.js
+++ b/app/assets/javascripts/logs/stores/mutations.js
@@ -112,7 +112,9 @@ export default {
},
// Managed apps data
[types.RECEIVE_MANAGED_APPS_DATA_SUCCESS](state, apps) {
- state.managedApps.options = apps;
+ state.managedApps.options = apps.filter(
+ ({ gitlab_managed_apps_logs_path }) => gitlab_managed_apps_logs_path, // eslint-disable-line babel/camelcase
+ );
state.managedApps.isLoading = false;
},
[types.RECEIVE_MANAGED_APPS_DATA_ERROR](state) {
diff --git a/app/assets/javascripts/logs/utils.js b/app/assets/javascripts/logs/utils.js
index 8479eeb3b59..8e537a4025f 100644
--- a/app/assets/javascripts/logs/utils.js
+++ b/app/assets/javascripts/logs/utils.js
@@ -1,5 +1,5 @@
-import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import dateFormat from 'dateformat';
+import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import { dateFormatMask } from './constants';
/**
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 3f85295a5ed..1572e82a66c 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -9,6 +9,8 @@ import './commons';
import './behaviors';
// lib/utils
+import applyGitLabUIConfig from '@gitlab/ui/dist/config';
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import {
handleLocationHash,
addSelectOnFocusBehaviour,
@@ -19,9 +21,7 @@ import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// everything else
import loadAwardsHandler from './awards_handler';
-import applyGitLabUIConfig from '@gitlab/ui/dist/config';
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import Flash, { removeFlashClickListener } from './flash';
+import { deprecatedCreateFlash as Flash, removeFlashClickListener } from './flash';
import './gl_dropdown';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
@@ -156,6 +156,9 @@ function deferredInitialisation() {
});
loadAwardsHandler();
+
+ // Adding a helper class to activate animations only after all is rendered
+ setTimeout(() => $body.addClass('page-initialised'), 1000);
}
document.addEventListener('DOMContentLoaded', () => {
diff --git a/app/assets/javascripts/maintenance_mode_settings/components/app.vue b/app/assets/javascripts/maintenance_mode_settings/components/app.vue
index 7798c443914..11d154ed9d1 100644
--- a/app/assets/javascripts/maintenance_mode_settings/components/app.vue
+++ b/app/assets/javascripts/maintenance_mode_settings/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlToggle, GlFormGroup, GlFormTextarea, GlDeprecatedButton } from '@gitlab/ui';
+import { GlToggle, GlFormGroup, GlFormTextarea, GlButton } from '@gitlab/ui';
export default {
name: 'MaintenanceModeSettingsApp',
@@ -7,7 +7,7 @@ export default {
GlToggle,
GlFormGroup,
GlFormTextarea,
- GlDeprecatedButton,
+ GlButton,
},
data() {
return {
@@ -38,7 +38,7 @@ export default {
/>
</gl-form-group>
<div class="mt-4">
- <gl-deprecated-button variant="success">{{ __('Save changes') }}</gl-deprecated-button>
+ <gl-button variant="success" category="primary">{{ __('Save changes') }}</gl-button>
</div>
</article>
</template>
diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js
index 683fe8b0b14..559efa4c66c 100644
--- a/app/assets/javascripts/manual_ordering.js
+++ b/app/assets/javascripts/manual_ordering.js
@@ -1,6 +1,6 @@
import Sortable from 'sortablejs';
import { s__ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
getBoardSortableDefaultOptions,
sortableStart,
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
index 70b18d9728d..3a67d0ad64a 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js
@@ -3,7 +3,7 @@
import Vue from 'vue';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import getModeByFileExtension from '~/lib/utils/ace_utils';
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index d8d203e0616..a5a930572e1 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import createFlash from '../flash';
+import { deprecatedCreateFlash as createFlash } from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
import MergeConflictsService from './merge_conflict_service';
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index a90e4e32d34..8322d36faee 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import TaskList from './task_list';
import MergeRequestTabs from './merge_request_tabs';
import IssuablesHelper from './helpers/issuables_helper';
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index e9b7b56a160..94b6ba7b1ce 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -5,7 +5,7 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
import createEventHub from '~/helpers/event_hub_factory';
import axios from './lib/utils/axios_utils';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown';
import {
@@ -21,6 +21,7 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
import Notes from './notes';
import { polyfillSticky } from './lib/utils/sticky';
+import initAddContextCommitsTriggers from './add_context_commits_modal';
import { __ } from './locale';
// MergeRequestTabs
@@ -166,8 +167,6 @@ export default class MergeRequestTabs {
if (this.setUrl) {
this.setCurrentAction(action);
}
-
- this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
}
}
}
@@ -251,6 +250,8 @@ export default class MergeRequestTabs {
}
}
}
+
+ this.eventHub.$emit('MergeRequestTabChange', action);
}
scrollToElement(container) {
@@ -340,6 +341,7 @@ export default class MergeRequestTabs {
this.scrollToElement('#commits');
this.toggleLoading(false);
+ initAddContextCommitsTriggers();
})
.catch(() => {
this.toggleLoading(false);
@@ -358,7 +360,11 @@ export default class MergeRequestTabs {
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath,
autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
- canRunPipeline: true,
+ canCreatePipelineInTargetProject: Boolean(
+ mrWidgetData?.can_create_pipeline_in_target_project,
+ ),
+ sourceProjectFullPath: mrWidgetData?.source_project_full_path || '',
+ targetProjectFullPath: mrWidgetData?.target_project_full_path || '',
projectId: pipelineTableViewEl.dataset.projectId,
mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
},
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 6aaba4e7c74..20d9fb82554 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
import { __ } from './locale';
diff --git a/app/assets/javascripts/milestones/project_milestone_combobox.vue b/app/assets/javascripts/milestones/project_milestone_combobox.vue
index 19148d6184f..d0179ab5509 100644
--- a/app/assets/javascripts/milestones/project_milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/project_milestone_combobox.vue
@@ -8,10 +8,10 @@ import {
GlSearchBoxByType,
GlIcon,
} from '@gitlab/ui';
+import { intersection, debounce } from 'lodash';
import { __, sprintf } from '~/locale';
import Api from '~/api';
-import createFlash from '~/flash';
-import { intersection, debounce } from 'lodash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
export default {
components: {
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index b39ad764f01..d4283701367 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js
index 5401fb7b6ec..cc787613c52 100644
--- a/app/assets/javascripts/mirrors/mirror_repos.js
+++ b/app/assets/javascripts/mirrors/mirror_repos.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { debounce } from 'lodash';
import { __ } from '~/locale';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import SSHMirror from './ssh_mirror';
diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js
index 986785fdfbe..eecfaa76168 100644
--- a/app/assets/javascripts/mirrors/ssh_mirror.js
+++ b/app/assets/javascripts/mirrors/ssh_mirror.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import { escape } from 'lodash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { backOff } from '~/lib/utils/common_utils';
import AUTH_METHOD from './constants';
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue
index 5562981fe1c..909ae2980d2 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget.vue
@@ -1,12 +1,12 @@
<script>
import { GlBadge, GlLoadingIcon, GlModalDirective, GlIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
+import { values, get } from 'lodash';
import { s__ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import AlertWidgetForm from './alert_widget_form.vue';
import AlertsService from '../services/alerts_service';
import { alertsValidator, queriesValidator } from '../validators';
import { OPERATORS } from '../constants';
-import { values, get } from 'lodash';
export default {
components: {
@@ -174,8 +174,8 @@ export default {
handleSetApiAction(apiAction) {
this.apiAction = apiAction;
},
- handleCreate({ operator, threshold, prometheus_metric_id }) {
- const newAlert = { operator, threshold, prometheus_metric_id };
+ handleCreate({ operator, threshold, prometheus_metric_id, runbookUrl }) {
+ const newAlert = { operator, threshold, prometheus_metric_id, runbookUrl };
this.isLoading = true;
this.service
.createAlert(newAlert)
@@ -189,8 +189,8 @@ export default {
this.isLoading = false;
});
},
- handleUpdate({ alert, operator, threshold }) {
- const updatedAlert = { operator, threshold };
+ handleUpdate({ alert, operator, threshold, runbookUrl }) {
+ const updatedAlert = { operator, threshold, runbookUrl };
this.isLoading = true;
this.service
.updateAlert(alert, updatedAlert)
diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
index b2d7ca0c4e0..5fa0da53a04 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget_form.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
@@ -7,8 +7,8 @@ import {
GlButtonGroup,
GlFormGroup,
GlFormInput,
- GlDropdown,
- GlDropdownItem,
+ GlNewDropdown as GlDropdown,
+ GlNewDropdownItem as GlDropdownItem,
GlModal,
GlTooltipDirective,
} from '@gitlab/ui';
@@ -88,6 +88,7 @@ export default {
operator: null,
threshold: null,
prometheusMetricId: null,
+ runbookUrl: null,
selectedAlert: {},
alertQuery: '',
};
@@ -116,7 +117,8 @@ export default {
this.operator &&
this.threshold === Number(this.threshold) &&
(this.operator !== this.selectedAlert.operator ||
- this.threshold !== this.selectedAlert.threshold)
+ this.threshold !== this.selectedAlert.threshold ||
+ this.runbookUrl !== this.selectedAlert.runbookUrl)
);
},
submitAction() {
@@ -153,13 +155,17 @@ export default {
const existingAlert = this.alertsToManage[existingAlertPath];
if (existingAlert) {
+ const { operator, threshold, runbookUrl } = existingAlert;
+
this.selectedAlert = existingAlert;
- this.operator = existingAlert.operator;
- this.threshold = existingAlert.threshold;
+ this.operator = operator;
+ this.threshold = threshold;
+ this.runbookUrl = runbookUrl;
} else {
this.selectedAlert = {};
this.operator = this.operators.greaterThan;
this.threshold = null;
+ this.runbookUrl = null;
}
this.prometheusMetricId = queryId;
@@ -168,13 +174,13 @@ export default {
this.resetAlertData();
this.$emit('cancel');
},
- handleSubmit(e) {
- e.preventDefault();
+ handleSubmit() {
this.$emit(this.submitAction, {
alert: this.selectedAlert.alert_path,
operator: this.operator,
threshold: this.threshold,
prometheus_metric_id: this.prometheusMetricId,
+ runbookUrl: this.runbookUrl,
});
},
handleShown() {
@@ -189,6 +195,7 @@ export default {
this.threshold = null;
this.prometheusMetricId = null;
this.selectedAlert = {};
+ this.runbookUrl = null;
},
getAlertFormActionTrackingOption() {
const label = `${this.submitAction}_alert`;
@@ -217,7 +224,7 @@ export default {
:modal-id="modalId"
:ok-variant="submitAction === 'delete' ? 'danger' : 'success'"
:ok-disabled="formDisabled"
- @ok="handleSubmit"
+ @ok.prevent="handleSubmit"
@hidden="handleHidden"
@shown="handleShown"
>
@@ -247,7 +254,7 @@ export default {
<gl-dropdown
id="alert-query-dropdown"
:text="queryDropdownLabel"
- toggle-class="dropdown-menu-toggle qa-alert-query-dropdown"
+ toggle-class="dropdown-menu-toggle gl-border-1! qa-alert-query-dropdown"
>
<gl-dropdown-item
v-for="query in relevantQueries"
@@ -259,7 +266,7 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
- <gl-button-group class="mb-2" :label="s__('PrometheusAlerts|Operator')">
+ <gl-button-group class="mb-3" :label="s__('PrometheusAlerts|Operator')">
<gl-deprecated-button
:class="{ active: operator === operators.greaterThan }"
:disabled="formDisabled"
@@ -294,6 +301,19 @@ export default {
data-qa-selector="alert_threshold_field"
/>
</gl-form-group>
+ <gl-form-group
+ :label="s__('PrometheusAlerts|Runbook URL (optional)')"
+ label-for="alert-runbook"
+ >
+ <gl-form-input
+ id="alert-runbook"
+ v-model="runbookUrl"
+ :disabled="formDisabled"
+ data-testid="alertRunbookField"
+ type="text"
+ :placeholder="s__('PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks')"
+ />
+ </gl-form-group>
</div>
<template #modal-ok>
<gl-link
diff --git a/app/assets/javascripts/monitoring/components/charts/gauge.vue b/app/assets/javascripts/monitoring/components/charts/gauge.vue
new file mode 100644
index 00000000000..63fa60bbdf0
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/gauge.vue
@@ -0,0 +1,122 @@
+<script>
+import { GlResizeObserverDirective } from '@gitlab/ui';
+import { GlGaugeChart } from '@gitlab/ui/dist/charts';
+import { isFinite, isArray, isInteger } from 'lodash';
+import { graphDataValidatorForValues } from '../../utils';
+import { getValidThresholds } from './options';
+import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
+
+export default {
+ components: {
+ GlGaugeChart,
+ },
+ directives: {
+ GlResizeObserverDirective,
+ },
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: graphDataValidatorForValues.bind(null, true),
+ },
+ },
+ data() {
+ return {
+ width: 0,
+ };
+ },
+ computed: {
+ rangeValues() {
+ let min = 0;
+ let max = 100;
+
+ const { minValue, maxValue } = this.graphData;
+
+ const isValidMinMax = () => {
+ return isFinite(minValue) && isFinite(maxValue) && minValue < maxValue;
+ };
+
+ if (isValidMinMax()) {
+ min = minValue;
+ max = maxValue;
+ }
+
+ return {
+ min,
+ max,
+ };
+ },
+ validThresholds() {
+ const { mode, values } = this.graphData?.thresholds || {};
+ const range = this.rangeValues;
+
+ if (!isArray(values)) {
+ return [];
+ }
+
+ return getValidThresholds({ mode, range, values });
+ },
+ queryResult() {
+ return this.graphData?.metrics[0]?.result[0]?.value[1];
+ },
+ splitValue() {
+ const { split } = this.graphData;
+ const defaultValue = 10;
+
+ return isInteger(split) && split > 0 ? split : defaultValue;
+ },
+ textValue() {
+ const formatFromPanel = this.graphData.format;
+ const defaultFormat = SUPPORTED_FORMATS.engineering;
+ const format = SUPPORTED_FORMATS[formatFromPanel] ?? defaultFormat;
+ const { queryResult } = this;
+
+ const formatter = getFormatter(format);
+
+ return isFinite(queryResult) ? formatter(queryResult) : '--';
+ },
+ thresholdsValue() {
+ /**
+ * If there are no valid thresholds, a default threshold
+ * will be set at 90% of the gauge arcs' max value
+ */
+ const { min, max } = this.rangeValues;
+
+ const defaultThresholdValue = [(max - min) * 0.95];
+ return this.validThresholds.length ? this.validThresholds : defaultThresholdValue;
+ },
+ value() {
+ /**
+ * The gauge chart gitlab-ui component expects a value
+ * of type number.
+ *
+ * So, if the query result is undefined,
+ * we pass the gauge chart a value of NaN.
+ */
+ return this.queryResult || NaN;
+ },
+ },
+ methods: {
+ onResize() {
+ if (!this.$refs.gaugeChart) return;
+ const { width } = this.$refs.gaugeChart.$el.getBoundingClientRect();
+ this.width = width;
+ },
+ },
+};
+</script>
+<template>
+ <div v-gl-resize-observer-directive="onResize">
+ <gl-gauge-chart
+ ref="gaugeChart"
+ v-bind="$attrs"
+ :value="value"
+ :min="rangeValues.min"
+ :max="rangeValues.max"
+ :thresholds="thresholdsValue"
+ :text="textValue"
+ :split-number="splitValue"
+ :width="width"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
index ddb44f7b1be..7003e2d37cf 100644
--- a/app/assets/javascripts/monitoring/components/charts/heatmap.vue
+++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
@@ -36,7 +36,7 @@ export default {
);
},
xAxisName() {
- return this.graphData.x_label || '';
+ return this.graphData.xLabel || '';
},
yAxisName() {
return this.graphData.y_label || '';
diff --git a/app/assets/javascripts/monitoring/components/charts/options.js b/app/assets/javascripts/monitoring/components/charts/options.js
index 42252dd5897..0cd4a02311c 100644
--- a/app/assets/javascripts/monitoring/components/charts/options.js
+++ b/app/assets/javascripts/monitoring/components/charts/options.js
@@ -1,6 +1,8 @@
+import { isFinite, uniq, sortBy, includes } from 'lodash';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import { __, s__ } from '~/locale';
import { formatDate, timezones, formats } from '../../format_date';
+import { thresholdModeTypes } from '../../constants';
const yAxisBoundaryGap = [0.1, 0.1];
/**
@@ -109,3 +111,65 @@ export const getTooltipFormatter = ({
const formatter = getFormatter(format);
return num => formatter(num, precision);
};
+
+// Thresholds
+
+/**
+ *
+ * Used to find valid thresholds for the gauge chart
+ *
+ * An array of thresholds values is
+ * - duplicate values are removed;
+ * - filtered for invalid values;
+ * - sorted in ascending order;
+ * - only first two values are used.
+ */
+export const getValidThresholds = ({ mode, range = {}, values = [] } = {}) => {
+ const supportedModes = [thresholdModeTypes.ABSOLUTE, thresholdModeTypes.PERCENTAGE];
+ const { min, max } = range;
+
+ /**
+ * return early if min and max have invalid values
+ * or mode has invalid value
+ */
+ if (!isFinite(min) || !isFinite(max) || min >= max || !includes(supportedModes, mode)) {
+ return [];
+ }
+
+ const uniqueThresholds = uniq(values);
+
+ const numberThresholds = uniqueThresholds.filter(threshold => isFinite(threshold));
+
+ const validThresholds = numberThresholds.filter(threshold => {
+ let isValid;
+
+ if (mode === thresholdModeTypes.PERCENTAGE) {
+ isValid = threshold > 0 && threshold < 100;
+ } else if (mode === thresholdModeTypes.ABSOLUTE) {
+ isValid = threshold > min && threshold < max;
+ }
+
+ return isValid;
+ });
+
+ const transformedThresholds = validThresholds.map(threshold => {
+ let transformedThreshold;
+
+ if (mode === 'percentage') {
+ transformedThreshold = (threshold / 100) * (max - min);
+ } else {
+ transformedThreshold = threshold;
+ }
+
+ return transformedThreshold;
+ });
+
+ const sortedThresholds = sortBy(transformedThresholds);
+
+ const reducedThresholdsArray =
+ sortedThresholds.length > 2
+ ? [sortedThresholds[0], sortedThresholds[1]]
+ : [...sortedThresholds];
+
+ return reducedThresholdsArray;
+};
diff --git a/app/assets/javascripts/monitoring/components/charts/single_stat.vue b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
index 106c76a97dc..a8ab41ebf26 100644
--- a/app/assets/javascripts/monitoring/components/charts/single_stat.vue
+++ b/app/assets/javascripts/monitoring/components/charts/single_stat.vue
@@ -50,7 +50,7 @@ export default {
}
formatter = getFormatter(SUPPORTED_FORMATS.number);
- return `${formatter(this.queryResult, defaultPrecision)}${this.queryInfo.unit}`;
+ return `${formatter(this.queryResult, defaultPrecision)}${this.queryInfo.unit ?? ''}`;
},
graphTitle() {
return this.queryInfo.label;
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index f2add429a80..054111c203e 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -1,6 +1,6 @@
<script>
-import { omit, throttle } from 'lodash';
-import { GlLink, GlDeprecatedButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
+import { isEmpty, omit, throttle } from 'lodash';
+import { GlLink, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
@@ -25,7 +25,6 @@ export default {
GlAreaChart,
GlLineChart,
GlTooltip,
- GlDeprecatedButton,
GlChartSeriesLabel,
GlLink,
Icon,
@@ -45,6 +44,11 @@ export default {
required: false,
default: () => ({}),
},
+ timeRange: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
seriesConfig: {
type: Object,
required: false,
@@ -174,10 +178,17 @@ export default {
chartOptions() {
const { yAxis, xAxis } = this.option;
const option = omit(this.option, ['series', 'yAxis', 'xAxis']);
+ const xAxisBounds = isEmpty(this.timeRange)
+ ? {}
+ : {
+ min: this.timeRange.start,
+ max: this.timeRange.end,
+ };
const timeXAxis = {
...getTimeAxisOptions({ timezone: this.timezone }),
...xAxis,
+ ...xAxisBounds,
};
const dataYAxis = {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index bde62275797..24aa7b3f504 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -2,12 +2,12 @@
import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import Mousetrap from 'mousetrap';
-import { GlIcon, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import DashboardHeader from './dashboard_header.vue';
import DashboardPanel from './dashboard_panel.vue';
import { s__ } from '~/locale';
-import createFlash from '~/flash';
-import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import Icon from '~/vue_shared/components/icon.vue';
@@ -34,7 +34,6 @@ export default {
DashboardHeader,
DashboardPanel,
Icon,
- GlIcon,
GlButton,
GraphGroup,
EmptyState,
@@ -48,11 +47,6 @@ export default {
TrackEvent: TrackEventDirective,
},
props: {
- externalDashboardUrl: {
- type: String,
- required: false,
- default: '',
- },
hasMetrics: {
type: Boolean,
required: false,
@@ -72,10 +66,6 @@ export default {
type: String,
required: true,
},
- addDashboardDocumentationPath: {
- type: String,
- required: true,
- },
settingsPath: {
type: String,
required: true,
@@ -320,7 +310,7 @@ export default {
},
onKeyup(event) {
const { key } = event;
- if (key === ESC_KEY || key === ESC_KEY_IE11) {
+ if (key === ESC_KEY) {
this.clearExpandedPanel();
}
},
@@ -398,7 +388,8 @@ export default {
},
},
i18n: {
- goBackLabel: s__('Metrics|Go back (Esc)'),
+ collapsePanelLabel: s__('Metrics|Collapse panel'),
+ collapsePanelTooltip: s__('Metrics|Collapse panel (Esc)'),
},
};
</script>
@@ -409,14 +400,11 @@ export default {
v-if="showHeader"
ref="prometheusGraphsHeader"
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
- :add-dashboard-documentation-path="addDashboardDocumentationPath"
:default-branch="defaultBranch"
:rearrange-panels-available="rearrangePanelsAvailable"
:custom-metrics-available="customMetricsAvailable"
:custom-metrics-path="customMetricsPath"
:validate-query-path="validateQueryPath"
- :external-dashboard-url="externalDashboardUrl"
- :has-metrics="hasMetrics"
:is-rearranging-panels="isRearrangingPanels"
:selected-time-range="selectedTimeRange"
@dateTimePickerInvalid="onDateTimePickerInvalid"
@@ -441,14 +429,10 @@ export default {
ref="goBackBtn"
v-gl-tooltip
class="mr-3 my-3"
- :title="$options.i18n.goBackLabel"
+ :title="$options.i18n.collapsePanelTooltip"
@click="onGoBack"
>
- <gl-icon
- name="arrow-left"
- :aria-label="$options.i18n.goBackLabel"
- class="text-secondary"
- />
+ {{ $options.i18n.collapsePanelLabel }}
</gl-button>
</template>
</dashboard-panel>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
new file mode 100644
index 00000000000..68afa2ace01
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -0,0 +1,291 @@
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex';
+import {
+ GlDeprecatedButton,
+ GlNewDropdown,
+ GlNewDropdownDivider,
+ GlNewDropdownItem,
+ GlModal,
+ GlIcon,
+ GlModalDirective,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
+import { PANEL_NEW_PAGE } from '../router/constants';
+import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
+import CreateDashboardModal from './create_dashboard_modal.vue';
+import { s__ } from '~/locale';
+import invalidUrl from '~/lib/utils/invalid_url';
+import { redirectTo } from '~/lib/utils/url_utility';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
+import { getAddMetricTrackingOptions } from '../utils';
+
+export default {
+ components: {
+ GlDeprecatedButton,
+ GlNewDropdown,
+ GlNewDropdownDivider,
+ GlNewDropdownItem,
+ GlModal,
+ GlIcon,
+ DuplicateDashboardModal,
+ CreateDashboardModal,
+ CustomMetricsFormFields,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
+ TrackEvent: TrackEventDirective,
+ },
+ props: {
+ addingMetricsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ customMetricsPath: {
+ type: String,
+ required: false,
+ default: invalidUrl,
+ },
+ validateQueryPath: {
+ type: String,
+ required: false,
+ default: invalidUrl,
+ },
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ isOotbDashboard: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return { customMetricsFormIsValid: null };
+ },
+ computed: {
+ ...mapState('monitoringDashboard', [
+ 'projectPath',
+ 'isUpdatingStarredValue',
+ 'addDashboardDocumentationPath',
+ ]),
+ ...mapGetters('monitoringDashboard', ['selectedDashboard']),
+ isOutOfTheBoxDashboard() {
+ return this.selectedDashboard?.out_of_the_box_dashboard;
+ },
+ isMenuItemEnabled() {
+ return {
+ addPanel: !this.isOotbDashboard,
+ createDashboard: Boolean(this.projectPath),
+ editDashboard: this.selectedDashboard?.can_edit,
+ };
+ },
+ isMenuItemShown() {
+ return {
+ duplicateDashboard: this.isOutOfTheBoxDashboard,
+ };
+ },
+ newPanelPageLocation() {
+ // Retains params/query if any
+ const { params, query } = this.$route ?? {};
+ return { name: PANEL_NEW_PAGE, params, query };
+ },
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', ['toggleStarredValue']),
+ setFormValidity(isValid) {
+ this.customMetricsFormIsValid = isValid;
+ },
+ hideAddMetricModal() {
+ this.$refs.addMetricModal.hide();
+ },
+ getAddMetricTrackingOptions,
+ submitCustomMetricsForm() {
+ this.$refs.customMetricsForm.submit();
+ },
+ selectDashboard(dashboard) {
+ // Once the sidebar See metrics link is updated to the new URL,
+ // this sort of hardcoding will not be necessary.
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/229277
+ const baseURL = `${this.projectPath}/-/metrics`;
+ const dashboardPath = encodeURIComponent(
+ dashboard.out_of_the_box_dashboard ? dashboard.path : dashboard.display_name,
+ );
+ redirectTo(`${baseURL}/${dashboardPath}`);
+ },
+ },
+
+ modalIds: {
+ addMetric: 'addMetric',
+ createDashboard: 'createDashboard',
+ duplicateDashboard: 'duplicateDashboard',
+ },
+ i18n: {
+ actionsMenu: s__('Metrics|More actions'),
+ duplicateDashboard: s__('Metrics|Duplicate current dashboard'),
+ starDashboard: s__('Metrics|Star dashboard'),
+ unstarDashboard: s__('Metrics|Unstar dashboard'),
+ addMetric: s__('Metrics|Add metric'),
+ addPanel: s__('Metrics|Add panel'),
+ addPanelInfo: s__('Metrics|Duplicate this dashboard to add panel or edit dashboard YAML.'),
+ editDashboardInfo: s__('Metrics|Duplicate this dashboard to add panel or edit dashboard YAML.'),
+ editDashboard: s__('Metrics|Edit dashboard YAML'),
+ createDashboard: s__('Metrics|Create new dashboard'),
+ },
+};
+</script>
+
+<template>
+ <!--
+ This component should be replaced with a variant developed
+ as part of https://gitlab.com/gitlab-org/gitlab-ui/-/issues/936
+ The variant will create a dropdown with an icon, no text and no caret
+ -->
+ <gl-new-dropdown
+ v-gl-tooltip
+ data-testid="actions-menu"
+ data-qa-selector="actions_menu_dropdown"
+ right
+ no-caret
+ toggle-class="gl-px-3!"
+ :title="$options.i18n.actionsMenu"
+ >
+ <template #button-content>
+ <gl-icon class="gl-mr-0!" name="ellipsis_v" />
+ </template>
+
+ <template v-if="addingMetricsAvailable">
+ <gl-new-dropdown-item
+ v-gl-modal="$options.modalIds.addMetric"
+ data-qa-selector="add_metric_button"
+ data-testid="add-metric-item"
+ >
+ {{ $options.i18n.addMetric }}
+ </gl-new-dropdown-item>
+ <gl-modal
+ ref="addMetricModal"
+ :modal-id="$options.modalIds.addMetric"
+ :title="$options.i18n.addMetric"
+ data-testid="add-metric-modal"
+ >
+ <form ref="customMetricsForm" :action="customMetricsPath" method="post">
+ <custom-metrics-form-fields
+ :validate-query-path="validateQueryPath"
+ form-operation="post"
+ @formValidation="setFormValidity"
+ />
+ </form>
+ <div slot="modal-footer">
+ <gl-deprecated-button @click="hideAddMetricModal">
+ {{ __('Cancel') }}
+ </gl-deprecated-button>
+ <gl-deprecated-button
+ v-track-event="getAddMetricTrackingOptions()"
+ data-testid="add-metric-modal-submit-button"
+ :disabled="!customMetricsFormIsValid"
+ variant="success"
+ @click="submitCustomMetricsForm"
+ >
+ {{ __('Save changes') }}
+ </gl-deprecated-button>
+ </div>
+ </gl-modal>
+ </template>
+
+ <gl-new-dropdown-item
+ v-if="isMenuItemEnabled.addPanel"
+ data-testid="add-panel-item-enabled"
+ :to="newPanelPageLocation"
+ >
+ {{ $options.i18n.addPanel }}
+ </gl-new-dropdown-item>
+
+ <!--
+ wrapper for tooltip as button can be `disabled`
+ https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
+ -->
+ <div v-else v-gl-tooltip :title="$options.i18n.addPanelInfo">
+ <gl-new-dropdown-item
+ :alt="$options.i18n.addPanelInfo"
+ :to="newPanelPageLocation"
+ data-testid="add-panel-item-disabled"
+ disabled
+ class="gl-cursor-not-allowed"
+ >
+ <span class="gl-text-gray-400">{{ $options.i18n.addPanel }}</span>
+ </gl-new-dropdown-item>
+ </div>
+
+ <gl-new-dropdown-item
+ v-if="isMenuItemEnabled.editDashboard"
+ :href="selectedDashboard ? selectedDashboard.project_blob_path : null"
+ data-qa-selector="edit_dashboard_button_enabled"
+ data-testid="edit-dashboard-item-enabled"
+ >
+ {{ $options.i18n.editDashboard }}
+ </gl-new-dropdown-item>
+
+ <!--
+ wrapper for tooltip as button can be `disabled`
+ https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
+ -->
+ <div v-else v-gl-tooltip :title="$options.i18n.editDashboardInfo">
+ <gl-new-dropdown-item
+ :alt="$options.i18n.editDashboardInfo"
+ :href="selectedDashboard ? selectedDashboard.project_blob_path : null"
+ data-testid="edit-dashboard-item-disabled"
+ disabled
+ class="gl-cursor-not-allowed"
+ >
+ <span class="gl-text-gray-400">{{ $options.i18n.editDashboard }}</span>
+ </gl-new-dropdown-item>
+ </div>
+
+ <template v-if="isMenuItemShown.duplicateDashboard">
+ <gl-new-dropdown-item
+ v-gl-modal="$options.modalIds.duplicateDashboard"
+ data-testid="duplicate-dashboard-item"
+ >
+ {{ $options.i18n.duplicateDashboard }}
+ </gl-new-dropdown-item>
+
+ <duplicate-dashboard-modal
+ :default-branch="defaultBranch"
+ :modal-id="$options.modalIds.duplicateDashboard"
+ data-testid="duplicate-dashboard-modal"
+ @dashboardDuplicated="selectDashboard"
+ />
+ </template>
+
+ <gl-new-dropdown-item
+ v-if="selectedDashboard"
+ data-testid="star-dashboard-item"
+ :disabled="isUpdatingStarredValue"
+ @click="toggleStarredValue()"
+ >
+ {{ selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard }}
+ </gl-new-dropdown-item>
+
+ <gl-new-dropdown-divider />
+
+ <gl-new-dropdown-item
+ v-gl-modal="$options.modalIds.createDashboard"
+ data-testid="create-dashboard-item"
+ :disabled="!isMenuItemEnabled.createDashboard"
+ :class="{ 'monitoring-actions-item-disabled': !isMenuItemEnabled.createDashboard }"
+ >
+ {{ $options.i18n.createDashboard }}
+ </gl-new-dropdown-item>
+
+ <template v-if="isMenuItemEnabled.createDashboard">
+ <create-dashboard-modal
+ data-testid="create-dashboard-modal"
+ :add-dashboard-documentation-path="addDashboardDocumentationPath"
+ :modal-id="$options.modalIds.createDashboard"
+ :project-path="projectPath"
+ />
+ </template>
+ </gl-new-dropdown>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index fe6ca3a2a07..6a7bf81c643 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -3,23 +3,14 @@ import { debounce } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import {
GlButton,
- GlIcon,
- GlDeprecatedButton,
- GlDropdown,
- GlDropdownItem,
- GlDropdownHeader,
- GlDropdownDivider,
GlNewDropdown,
- GlNewDropdownDivider,
- GlNewDropdownItem,
- GlModal,
GlLoadingIcon,
+ GlNewDropdownItem,
+ GlNewDropdownHeader,
GlSearchBoxByType,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
-import { s__ } from '~/locale';
-import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import Icon from '~/vue_shared/components/icon.vue';
@@ -27,11 +18,9 @@ import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_p
import DashboardsDropdown from './dashboards_dropdown.vue';
import RefreshButton from './refresh_button.vue';
-import CreateDashboardModal from './create_dashboard_modal.vue';
-import DuplicateDashboardModal from './duplicate_dashboard_modal.vue';
+import ActionsMenu from './dashboard_actions_menu.vue';
-import TrackEventDirective from '~/vue_shared/directives/track_event';
-import { getAddMetricTrackingOptions, timeRangeToUrl } from '../utils';
+import { timeRangeToUrl } from '../utils';
import { timeRanges } from '~/vue_shared/constants';
import { timezones } from '../format_date';
@@ -39,30 +28,22 @@ export default {
components: {
Icon,
GlButton,
- GlIcon,
- GlDeprecatedButton,
- GlDropdown,
- GlLoadingIcon,
- GlDropdownItem,
- GlDropdownHeader,
- GlDropdownDivider,
GlNewDropdown,
- GlNewDropdownDivider,
+ GlLoadingIcon,
GlNewDropdownItem,
+ GlNewDropdownHeader,
+
GlSearchBoxByType,
- GlModal,
- CustomMetricsFormFields,
DateTimePicker,
DashboardsDropdown,
RefreshButton,
- DuplicateDashboardModal,
- CreateDashboardModal,
+
+ ActionsMenu,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
- TrackEvent: TrackEventDirective,
},
props: {
defaultBranch: {
@@ -89,16 +70,6 @@ export default {
required: false,
default: invalidUrl,
},
- externalDashboardUrl: {
- type: String,
- required: false,
- default: '',
- },
- hasMetrics: {
- type: Boolean,
- required: false,
- default: true,
- },
isRearrangingPanels: {
type: Boolean,
required: true,
@@ -107,32 +78,20 @@ export default {
type: Object,
required: true,
},
- addDashboardDocumentationPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- formIsValid: null,
- };
},
computed: {
...mapState('monitoringDashboard', [
'emptyState',
'environmentsLoading',
'currentEnvironmentName',
- 'isUpdatingStarredValue',
'dashboardTimezone',
'projectPath',
'canAccessOperationsSettings',
'operationsSettingsPath',
'currentDashboard',
+ 'externalDashboardUrl',
]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
- isOutOfTheBoxDashboard() {
- return this.selectedDashboard?.out_of_the_box_dashboard;
- },
shouldShowEmptyState() {
return Boolean(this.emptyState);
},
@@ -146,24 +105,27 @@ export default {
// Custom metrics only avaialble on system dashboards because
// they are stored in the database. This can be improved. See:
// https://gitlab.com/gitlab-org/gitlab/-/issues/28241
- this.selectedDashboard?.system_dashboard
+ this.selectedDashboard?.out_of_the_box_dashboard
);
},
showRearrangePanelsBtn() {
return !this.shouldShowEmptyState && this.rearrangePanelsAvailable;
},
+ environmentDropdownText() {
+ return this.currentEnvironmentName ?? '';
+ },
displayUtc() {
return this.dashboardTimezone === timezones.UTC;
},
- shouldShowActionsMenu() {
- return Boolean(this.projectPath);
- },
shouldShowSettingsButton() {
return this.canAccessOperationsSettings && this.operationsSettingsPath;
},
+ isOOTBDashboard() {
+ return this.selectedDashboard?.out_of_the_box_dashboard ?? false;
+ },
},
methods: {
- ...mapActions('monitoringDashboard', ['filterEnvironments', 'toggleStarredValue']),
+ ...mapActions('monitoringDashboard', ['filterEnvironments']),
selectDashboard(dashboard) {
// Once the sidebar See metrics link is updated to the new URL,
// this sort of hardcoding will not be necessary.
@@ -187,16 +149,6 @@ export default {
toggleRearrangingPanels() {
this.$emit('setRearrangingPanels', !this.isRearrangingPanels);
},
- setFormValidity(isValid) {
- this.formIsValid = isValid;
- },
- hideAddMetricModal() {
- this.$refs.addMetricModal.hide();
- },
- getAddMetricTrackingOptions,
- submitCustomMetricsForm() {
- this.$refs.customMetricsForm.submit();
- },
getEnvironmentPath(environment) {
// Once the sidebar See metrics link is updated to the new URL,
// this sort of hardcoding will not be necessary.
@@ -209,16 +161,6 @@ export default {
return mergeUrlParams({ environment }, url);
},
},
- modalIds: {
- addMetric: 'addMetric',
- createDashboard: 'createDashboard',
- duplicateDashboard: 'duplicateDashboard',
- },
- i18n: {
- starDashboard: s__('Metrics|Star dashboard'),
- unstarDashboard: s__('Metrics|Unstar dashboard'),
- addMetric: s__('Metrics|Add metric'),
- },
timeRanges,
};
</script>
@@ -232,7 +174,6 @@ export default {
class="flex-grow-1"
toggle-class="dropdown-menu-toggle"
:default-branch="defaultBranch"
- :modal-id="$options.modalIds.duplicateDashboard"
@selectDashboard="selectDashboard"
/>
</div>
@@ -240,39 +181,30 @@ export default {
<span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
<div class="mb-2 pr-2 d-flex d-sm-block">
- <gl-dropdown
+ <gl-new-dropdown
id="monitor-environments-dropdown"
ref="monitorEnvironmentsDropdown"
class="flex-grow-1"
data-qa-selector="environments_dropdown"
toggle-class="dropdown-menu-toggle"
menu-class="monitor-environment-dropdown-menu"
- :text="currentEnvironmentName"
+ :text="environmentDropdownText"
>
<div class="d-flex flex-column overflow-hidden">
- <gl-dropdown-header class="monitor-environment-dropdown-header text-center">
- {{ __('Environment') }}
- </gl-dropdown-header>
- <gl-dropdown-divider />
- <gl-search-box-by-type
- ref="monitorEnvironmentsDropdownSearch"
- class="m-2"
- @input="debouncedEnvironmentsSearch"
- />
- <gl-loading-icon
- v-if="environmentsLoading"
- ref="monitorEnvironmentsDropdownLoading"
- :inline="true"
- />
+ <gl-new-dropdown-header>{{ __('Environment') }}</gl-new-dropdown-header>
+ <gl-search-box-by-type class="m-2" @input="debouncedEnvironmentsSearch" />
+
+ <gl-loading-icon v-if="environmentsLoading" :inline="true" />
<div v-else class="flex-fill overflow-auto">
- <gl-dropdown-item
+ <gl-new-dropdown-item
v-for="environment in filteredEnvironments"
:key="environment.id"
- :active="environment.name === currentEnvironmentName"
- active-class="is-active"
+ :is-check-item="true"
+ :is-checked="environment.name === currentEnvironmentName"
:href="getEnvironmentPath(environment.id)"
- >{{ environment.name }}</gl-dropdown-item
>
+ {{ environment.name }}
+ </gl-new-dropdown-item>
</div>
<div
v-show="shouldShowEnvironmentsDropdownNoMatchedMsg"
@@ -282,7 +214,7 @@ export default {
{{ __('No matching results') }}
</div>
</div>
- </gl-dropdown>
+ </gl-new-dropdown>
</div>
<div class="mb-2 pr-2 d-flex d-sm-block">
@@ -305,163 +237,56 @@ export default {
<div class="flex-grow-1"></div>
<div class="d-sm-flex">
- <div v-if="selectedDashboard" class="mb-2 mr-2 d-flex">
- <!--
- wrapper for tooltip as button can be `disabled`
- https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
- -->
- <div
- v-gl-tooltip
- class="flex-grow-1"
- :title="
- selectedDashboard.starred ? $options.i18n.unstarDashboard : $options.i18n.starDashboard
- "
- >
- <gl-deprecated-button
- ref="toggleStarBtn"
- class="w-100"
- :disabled="isUpdatingStarredValue"
- variant="default"
- @click="toggleStarredValue()"
- >
- <gl-icon :name="selectedDashboard.starred ? 'star' : 'star-o'" />
- </gl-deprecated-button>
- </div>
- </div>
-
<div v-if="showRearrangePanelsBtn" class="mb-2 mr-2 d-flex">
- <gl-deprecated-button
+ <gl-button
:pressed="isRearrangingPanels"
variant="default"
class="flex-grow-1 js-rearrange-button"
@click="toggleRearrangingPanels"
>
{{ __('Arrange charts') }}
- </gl-deprecated-button>
- </div>
- <div v-if="addingMetricsAvailable" class="mb-2 mr-2 d-flex d-sm-block">
- <gl-deprecated-button
- ref="addMetricBtn"
- v-gl-modal="$options.modalIds.addMetric"
- variant="outline-success"
- data-qa-selector="add_metric_button"
- class="flex-grow-1"
- >
- {{ $options.i18n.addMetric }}
- </gl-deprecated-button>
- <gl-modal
- ref="addMetricModal"
- :modal-id="$options.modalIds.addMetric"
- :title="$options.i18n.addMetric"
- >
- <form ref="customMetricsForm" :action="customMetricsPath" method="post">
- <custom-metrics-form-fields
- :validate-query-path="validateQueryPath"
- form-operation="post"
- @formValidation="setFormValidity"
- />
- </form>
- <div slot="modal-footer">
- <gl-deprecated-button @click="hideAddMetricModal">
- {{ __('Cancel') }}
- </gl-deprecated-button>
- <gl-deprecated-button
- ref="submitCustomMetricsFormBtn"
- v-track-event="getAddMetricTrackingOptions()"
- :disabled="!formIsValid"
- variant="success"
- @click="submitCustomMetricsForm"
- >
- {{ __('Save changes') }}
- </gl-deprecated-button>
- </div>
- </gl-modal>
- </div>
-
- <div
- v-if="selectedDashboard && selectedDashboard.can_edit"
- class="mb-2 mr-2 d-flex d-sm-block"
- >
- <gl-deprecated-button
- class="flex-grow-1 js-edit-link"
- :href="selectedDashboard.project_blob_path"
- data-qa-selector="edit_dashboard_button"
- >
- {{ __('Edit dashboard') }}
- </gl-deprecated-button>
+ </gl-button>
</div>
<div
v-if="externalDashboardUrl && externalDashboardUrl.length"
class="mb-2 mr-2 d-flex d-sm-block"
>
- <gl-deprecated-button
+ <gl-button
class="flex-grow-1 js-external-dashboard-link"
- variant="primary"
+ variant="info"
+ category="primary"
:href="externalDashboardUrl"
target="_blank"
rel="noopener noreferrer"
>
{{ __('View full dashboard') }} <icon name="external-link" />
- </gl-deprecated-button>
+ </gl-button>
</div>
- <!-- This separator should be displayed only if at least one of the action menu or settings button are displayed -->
- <span
- v-if="shouldShowActionsMenu || shouldShowSettingsButton"
- aria-hidden="true"
- class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"
- ></span>
+ <div class="gl-mb-3 gl-mr-3 d-flex d-sm-block">
+ <actions-menu
+ :adding-metrics-available="addingMetricsAvailable"
+ :custom-metrics-path="customMetricsPath"
+ :validate-query-path="validateQueryPath"
+ :default-branch="defaultBranch"
+ :is-ootb-dashboard="isOOTBDashboard"
+ />
+ </div>
- <div v-if="shouldShowActionsMenu" class="gl-mb-3 gl-mr-3 d-flex d-sm-block">
- <gl-new-dropdown
- v-gl-tooltip
- right
- class="gl-flex-grow-1"
- data-testid="actions-menu"
- :title="s__('Metrics|Create dashboard')"
- :icon="'plus-square'"
- >
- <gl-new-dropdown-item
- v-gl-modal="$options.modalIds.createDashboard"
- data-testid="action-create-dashboard"
- >{{ s__('Metrics|Create new dashboard') }}</gl-new-dropdown-item
- >
+ <template v-if="shouldShowSettingsButton">
+ <span aria-hidden="true" class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"></span>
- <create-dashboard-modal
- data-testid="create-dashboard-modal"
- :add-dashboard-documentation-path="addDashboardDocumentationPath"
- :modal-id="$options.modalIds.createDashboard"
- :project-path="projectPath"
+ <div class="mb-2 mr-2 d-flex d-sm-block">
+ <gl-button
+ v-gl-tooltip
+ data-testid="metrics-settings-button"
+ icon="settings"
+ :href="operationsSettingsPath"
+ :title="s__('Metrics|Metrics Settings')"
/>
-
- <template v-if="isOutOfTheBoxDashboard">
- <gl-new-dropdown-divider />
- <gl-new-dropdown-item
- ref="duplicateDashboardItem"
- v-gl-modal="$options.modalIds.duplicateDashboard"
- data-testid="action-duplicate-dashboard"
- >
- {{ s__('Metrics|Duplicate current dashboard') }}
- </gl-new-dropdown-item>
- </template>
- </gl-new-dropdown>
- </div>
-
- <div v-if="shouldShowSettingsButton" class="mb-2 mr-2 d-flex d-sm-block">
- <gl-button
- v-gl-tooltip
- data-testid="metrics-settings-button"
- icon="settings"
- :href="operationsSettingsPath"
- :title="s__('Metrics|Metrics Settings')"
- />
- </div>
+ </div>
+ </template>
</div>
- <duplicate-dashboard-modal
- :default-branch="defaultBranch"
- :modal-id="$options.modalIds.duplicateDashboard"
- @dashboardDuplicated="selectDashboard"
- />
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 3e3c8408de3..278858d3a94 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -1,20 +1,23 @@
<script>
import { mapState } from 'vuex';
-import { pickBy } from 'lodash';
-import invalidUrl from '~/lib/utils/invalid_url';
-import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
+import { mapValues, pickBy } from 'lodash';
import {
GlResizeObserverDirective,
GlIcon,
+ GlLink,
GlLoadingIcon,
GlNewDropdown as GlDropdown,
GlNewDropdownItem as GlDropdownItem,
GlNewDropdownDivider as GlDropdownDivider,
GlModal,
GlModalDirective,
+ GlSprintf,
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
+import invalidUrl from '~/lib/utils/invalid_url';
+import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import { __, n__ } from '~/locale';
import { panelTypes } from '../constants';
@@ -22,6 +25,7 @@ import MonitorEmptyChart from './charts/empty_chart.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorAnomalyChart from './charts/anomaly.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
+import MonitorGaugeChart from './charts/gauge.vue';
import MonitorHeatmapChart from './charts/heatmap.vue';
import MonitorColumnChart from './charts/column.vue';
import MonitorBarChart from './charts/bar.vue';
@@ -30,6 +34,7 @@ import MonitorStackedColumnChart from './charts/stacked_column.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import AlertWidget from './alert_widget.vue';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
+import { graphDataToCsv } from '../csv_export';
const events = {
timeRangeZoom: 'timerangezoom',
@@ -41,12 +46,14 @@ export default {
MonitorEmptyChart,
AlertWidget,
GlIcon,
+ GlLink,
GlLoadingIcon,
GlTooltip,
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlModal,
+ GlSprintf,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
@@ -128,6 +135,15 @@ export default {
return getters[`${this.namespace}/selectedDashboard`];
},
}),
+ fixedCurrentTimeRange() {
+ // convertToFixedRange throws an error if the time range
+ // is not properly set.
+ try {
+ return convertToFixedRange(this.timeRange);
+ } catch {
+ return {};
+ }
+ },
title() {
return this.graphData?.title || '';
},
@@ -148,13 +164,10 @@ export default {
return null;
},
csvText() {
- const chartData = this.graphData?.metrics[0].result[0].values || [];
- const yLabel = this.graphData.y_label;
- const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/require-i18n-strings
- return chartData.reduce((csv, data) => {
- const row = data.join(',');
- return `${csv}${row}\r\n`;
- }, header);
+ if (this.graphData) {
+ return graphDataToCsv(this.graphData);
+ }
+ return null;
},
downloadCsv() {
const data = new Blob([this.csvText], { type: 'text/plain' });
@@ -172,6 +185,9 @@ export default {
if (this.isPanelType(panelTypes.SINGLE_STAT)) {
return MonitorSingleStatChart;
}
+ if (this.isPanelType(panelTypes.GAUGE_CHART)) {
+ return MonitorGaugeChart;
+ }
if (this.isPanelType(panelTypes.HEATMAP)) {
return MonitorHeatmapChart;
}
@@ -217,7 +233,8 @@ export default {
return (
this.isPanelType(panelTypes.AREA_CHART) ||
this.isPanelType(panelTypes.LINE_CHART) ||
- this.isPanelType(panelTypes.SINGLE_STAT)
+ this.isPanelType(panelTypes.SINGLE_STAT) ||
+ this.isPanelType(panelTypes.GAUGE_CHART)
);
},
editCustomMetricLink() {
@@ -328,6 +345,19 @@ export default {
this.$refs.copyChartLink.$el.firstChild.click();
}
},
+ getAlertRunbooks(queries) {
+ const hasRunbook = alert => Boolean(alert.runbookUrl);
+ const graphAlertsWithRunbooks = pickBy(this.getGraphAlerts(queries), hasRunbook);
+ const alertToRunbookTransform = alert => {
+ const alertQuery = queries.find(query => query.metricId === alert.metricId);
+ return {
+ key: alert.metricId,
+ href: alert.runbookUrl,
+ label: alertQuery.label,
+ };
+ };
+ return mapValues(graphAlertsWithRunbooks, alertToRunbookTransform);
+ },
},
panelTypes,
};
@@ -364,15 +394,21 @@ export default {
data-qa-selector="prometheus_graph_widgets"
>
<div data-testid="dropdown-wrapper" class="d-flex align-items-center">
+ <!--
+ This component should be replaced with a variant developed
+ as part of https://gitlab.com/gitlab-org/gitlab-ui/-/issues/936
+ The variant will create a dropdown with an icon, no text and no caret
+ -->
<gl-dropdown
v-gl-tooltip
- toggle-class="shadow-none border-0"
+ toggle-class="gl-px-3!"
+ no-caret
data-qa-selector="prometheus_widgets_dropdown"
right
:title="__('More actions')"
>
- <template slot="button-content">
- <gl-icon name="ellipsis_v" class="dropdown-icon text-secondary" />
+ <template #button-content>
+ <gl-icon class="gl-mr-0!" name="ellipsis_v" />
</template>
<gl-dropdown-item
v-if="expandBtnAvailable"
@@ -423,6 +459,25 @@ export default {
>
{{ __('Alerts') }}
</gl-dropdown-item>
+ <gl-dropdown-item
+ v-for="runbook in getAlertRunbooks(graphData.metrics)"
+ :key="runbook.key"
+ :href="safeUrl(runbook.href)"
+ data-testid="runbookLink"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <span>
+ <gl-sprintf :message="s__('Metrics|View runbook - %{label}')">
+ <template #label>
+ {{ runbook.label }}
+ </template>
+ </gl-sprintf>
+ </span>
+ <gl-icon name="external-link" />
+ </span>
+ </gl-dropdown-item>
<template v-if="graphData.links && graphData.links.length">
<gl-dropdown-divider />
@@ -465,6 +520,7 @@ export default {
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
:timezone="dashboardTimezone"
+ :time-range="fixedCurrentTimeRange"
v-bind="$attrs"
v-on="$listeners"
@datazoom="onDatazoom"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
new file mode 100644
index 00000000000..88d5a35146f
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
@@ -0,0 +1,199 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import {
+ GlCard,
+ GlForm,
+ GlFormGroup,
+ GlFormTextarea,
+ GlButton,
+ GlSprintf,
+ GlAlert,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
+import { timeRanges } from '~/vue_shared/constants';
+import DashboardPanel from './dashboard_panel.vue';
+
+const initialYml = `title: Go heap size
+type: area-chart
+y_axis:
+ format: 'bytes'
+metrics:
+ - metric_id: 'go_memstats_alloc_bytes_1'
+ query_range: 'go_memstats_alloc_bytes'
+`;
+
+export default {
+ components: {
+ GlCard,
+ GlForm,
+ GlFormGroup,
+ GlFormTextarea,
+ GlButton,
+ GlSprintf,
+ GlAlert,
+ DashboardPanel,
+ DateTimePicker,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ data() {
+ return {
+ yml: initialYml,
+ };
+ },
+ computed: {
+ ...mapState('monitoringDashboard', [
+ 'panelPreviewIsLoading',
+ 'panelPreviewError',
+ 'panelPreviewGraphData',
+ 'panelPreviewTimeRange',
+ 'panelPreviewIsShown',
+ 'projectPath',
+ 'addDashboardDocumentationPath',
+ ]),
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', [
+ 'fetchPanelPreview',
+ 'fetchPanelPreviewMetrics',
+ 'setPanelPreviewTimeRange',
+ ]),
+ onSubmit() {
+ this.fetchPanelPreview(this.yml);
+ },
+ onDateTimePickerInput(timeRange) {
+ this.setPanelPreviewTimeRange(timeRange);
+ // refetch data only if preview has been clicked
+ // and there are no errors
+ if (this.panelPreviewIsShown && !this.panelPreviewError) {
+ this.fetchPanelPreviewMetrics();
+ }
+ },
+ onRefresh() {
+ // refetch data only if preview has been clicked
+ // and there are no errors
+ if (this.panelPreviewIsShown && !this.panelPreviewError) {
+ this.fetchPanelPreviewMetrics();
+ }
+ },
+ },
+ timeRanges,
+};
+</script>
+<template>
+ <div class="prometheus-panel-builder">
+ <div class="gl-xs-flex-direction-column gl-display-flex gl-mx-n3">
+ <gl-card class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3">
+ <template #header>
+ <h2 class="gl-font-size-h2 gl-my-3">{{ s__('Metrics|1. Define and preview panel') }}</h2>
+ </template>
+ <template #default>
+ <p>{{ s__('Metrics|Define panel YAML below to preview panel.') }}</p>
+ <gl-form @submit.prevent="onSubmit">
+ <gl-form-group :label="s__('Metrics|Panel YAML')" label-for="panel-yml-input">
+ <gl-form-textarea
+ id="panel-yml-input"
+ v-model="yml"
+ class="gl-h-200! gl-font-monospace! gl-font-size-monospace!"
+ />
+ </gl-form-group>
+ <div class="gl-text-right">
+ <gl-button
+ ref="clipboardCopyBtn"
+ variant="success"
+ category="secondary"
+ :data-clipboard-text="yml"
+ class="gl-xs-w-full gl-xs-mb-3"
+ @click="$toast.show(s__('Metrics|Panel YAML copied'))"
+ >
+ {{ s__('Metrics|Copy YAML') }}
+ </gl-button>
+ <gl-button
+ type="submit"
+ variant="success"
+ :disabled="panelPreviewIsLoading"
+ class="js-no-auto-disable gl-xs-w-full"
+ >
+ {{ s__('Metrics|Preview panel') }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </template>
+ </gl-card>
+
+ <gl-card
+ class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3"
+ body-class="gl-display-flex gl-flex-direction-column"
+ >
+ <template #header>
+ <h2 class="gl-font-size-h2 gl-my-3">
+ {{ s__('Metrics|2. Paste panel YAML into dashboard') }}
+ </h2>
+ </template>
+ <template #default>
+ <div
+ class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-justify-content-center"
+ >
+ <p>
+ {{ s__('Metrics|Copy and paste the panel YAML into your dashboard YAML file.') }}
+ <br />
+ <gl-sprintf
+ :message="
+ s__(
+ 'Metrics|Dashboard files can be found in %{codeStart}.gitlab/dashboards%{codeEnd} at the root of this project.',
+ )
+ "
+ >
+ <template #code="{content}">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+
+ <div class="gl-text-right">
+ <gl-button
+ ref="viewDocumentationBtn"
+ category="secondary"
+ class="gl-xs-w-full gl-xs-mb-3"
+ variant="info"
+ target="_blank"
+ :href="addDashboardDocumentationPath"
+ >
+ {{ s__('Metrics|View documentation') }}
+ </gl-button>
+ <gl-button
+ ref="openRepositoryBtn"
+ variant="success"
+ :href="projectPath"
+ class="gl-xs-w-full"
+ >
+ {{ s__('Metrics|Open repository') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-card>
+ </div>
+
+ <gl-alert v-if="panelPreviewError" variant="warning" :dismissible="false">
+ {{ panelPreviewError }}
+ </gl-alert>
+ <date-time-picker
+ ref="dateTimePicker"
+ class="gl-flex-grow-1 preview-date-time-picker gl-xs-mb-3"
+ :value="panelPreviewTimeRange"
+ :options="$options.timeRanges"
+ @input="onDateTimePickerInput"
+ />
+ <gl-button
+ v-gl-tooltip
+ data-testid="previewRefreshButton"
+ icon="retry"
+ :title="s__('Metrics|Refresh Prometheus data')"
+ @click="onRefresh"
+ />
+ <dashboard-panel :graph-data="panelPreviewGraphData" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index 574f48a72fe..aed27b5ea51 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -1,11 +1,11 @@
<script>
-import { mapState, mapActions, mapGetters } from 'vuex';
+import { mapState, mapGetters } from 'vuex';
import {
GlIcon,
- GlDropdown,
- GlDropdownItem,
- GlDropdownHeader,
- GlDropdownDivider,
+ GlNewDropdown,
+ GlNewDropdownItem,
+ GlNewDropdownHeader,
+ GlNewDropdownDivider,
GlSearchBoxByType,
GlModalDirective,
} from '@gitlab/ui';
@@ -17,10 +17,10 @@ const events = {
export default {
components: {
GlIcon,
- GlDropdown,
- GlDropdownItem,
- GlDropdownHeader,
- GlDropdownDivider,
+ GlNewDropdown,
+ GlNewDropdownItem,
+ GlNewDropdownHeader,
+ GlNewDropdownDivider,
GlSearchBoxByType,
},
directives: {
@@ -31,10 +31,6 @@ export default {
type: String,
required: true,
},
- modalId: {
- type: String,
- required: true,
- },
},
data() {
return {
@@ -44,9 +40,6 @@ export default {
computed: {
...mapState('monitoringDashboard', ['allDashboards']),
...mapGetters('monitoringDashboard', ['selectedDashboard']),
- isOutOfTheBoxDashboard() {
- return this.selectedDashboard?.out_of_the_box_dashboard;
- },
selectedDashboardText() {
return this.selectedDashboard?.display_name;
},
@@ -70,7 +63,6 @@ export default {
},
},
methods: {
- ...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
dashboardDisplayName(dashboard) {
return dashboard.display_name || dashboard.path || '';
},
@@ -81,16 +73,13 @@ export default {
};
</script>
<template>
- <gl-dropdown
+ <gl-new-dropdown
toggle-class="dropdown-menu-toggle"
menu-class="monitor-dashboard-dropdown-menu"
:text="selectedDashboardText"
>
<div class="d-flex flex-column overflow-hidden">
- <gl-dropdown-header class="monitor-dashboard-dropdown-header text-center">{{
- __('Dashboard')
- }}</gl-dropdown-header>
- <gl-dropdown-divider />
+ <gl-new-dropdown-header>{{ __('Dashboard') }}</gl-new-dropdown-header>
<gl-search-box-by-type
ref="monitorDashboardsDropdownSearch"
v-model="searchTerm"
@@ -98,33 +87,36 @@ export default {
/>
<div class="flex-fill overflow-auto">
- <gl-dropdown-item
+ <gl-new-dropdown-item
v-for="dashboard in starredDashboards"
:key="dashboard.path"
- :active="dashboard.path === selectedDashboardPath"
- active-class="is-active"
+ :is-check-item="true"
+ :is-checked="dashboard.path === selectedDashboardPath"
@click="selectDashboard(dashboard)"
>
- <div class="d-flex">
- {{ dashboardDisplayName(dashboard) }}
- <gl-icon class="text-muted ml-auto" name="star" />
+ <div class="gl-display-flex">
+ <div class="gl-flex-grow-1 gl-min-w-0">
+ <div class="gl-word-break-all">
+ {{ dashboardDisplayName(dashboard) }}
+ </div>
+ </div>
+ <gl-icon class="text-muted gl-flex-shrink-0" name="star" />
</div>
- </gl-dropdown-item>
-
- <gl-dropdown-divider
+ </gl-new-dropdown-item>
+ <gl-new-dropdown-divider
v-if="starredDashboards.length && nonStarredDashboards.length"
ref="starredListDivider"
/>
- <gl-dropdown-item
+ <gl-new-dropdown-item
v-for="dashboard in nonStarredDashboards"
:key="dashboard.path"
- :active="dashboard.path === selectedDashboardPath"
- active-class="is-active"
+ :is-check-item="true"
+ :is-checked="dashboard.path === selectedDashboardPath"
@click="selectDashboard(dashboard)"
>
{{ dashboardDisplayName(dashboard) }}
- </gl-dropdown-item>
+ </gl-new-dropdown-item>
</div>
<div
@@ -134,18 +126,6 @@ export default {
>
{{ __('No matching results') }}
</div>
-
- <!--
- This Duplicate Dashboard item will be removed from the dashboards dropdown
- in https://gitlab.com/gitlab-org/gitlab/-/issues/223223
- -->
- <template v-if="isOutOfTheBoxDashboard">
- <gl-dropdown-divider />
-
- <gl-dropdown-item v-gl-modal="modalId" data-testid="duplicateDashboardItem">
- {{ s__('Metrics|Duplicate dashboard') }}
- </gl-dropdown-item>
- </template>
</div>
- </gl-dropdown>
+ </gl-new-dropdown>
</template>
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
index 001cd0d47f1..db5b853d451 100644
--- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
@@ -1,7 +1,7 @@
<script>
-import { __, s__, sprintf } from '~/locale';
import { GlFormGroup, GlFormInput, GlFormRadioGroup, GlFormTextarea } from '@gitlab/ui';
import { escape as esc } from 'lodash';
+import { __, s__, sprintf } from '~/locale';
const defaultFileName = dashboard => dashboard.path.split('/').reverse()[0];
diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue
index dee4e5998ee..9cf492dd537 100644
--- a/app/assets/javascripts/monitoring/components/group_empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue
@@ -1,6 +1,6 @@
<script>
-import { __, sprintf } from '~/locale';
import { GlEmptyState } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import { metricStates } from '../constants';
export default {
diff --git a/app/assets/javascripts/monitoring/components/links_section.vue b/app/assets/javascripts/monitoring/components/links_section.vue
index 98b07d17694..ca1e9c4d0d4 100644
--- a/app/assets/javascripts/monitoring/components/links_section.vue
+++ b/app/assets/javascripts/monitoring/components/links_section.vue
@@ -23,7 +23,7 @@ export default {
class="gl-mb-1 gl-mr-5 gl-display-flex gl-display-sm-block gl-hover-text-blue-600-children gl-word-break-all"
>
<gl-link :href="link.url" class="gl-text-gray-900 gl-text-decoration-none!"
- ><gl-icon name="link" class="gl-text-gray-700 gl-vertical-align-text-bottom gl-mr-2" />{{
+ ><gl-icon name="link" class="gl-text-gray-500 gl-vertical-align-text-bottom gl-mr-2" />{{
link.title
}}
</gl-link>
diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue
index 5481806c3e0..0e9605450ed 100644
--- a/app/assets/javascripts/monitoring/components/refresh_button.vue
+++ b/app/assets/javascripts/monitoring/components/refresh_button.vue
@@ -1,7 +1,6 @@
<script>
-import { n__, __ } from '~/locale';
+import Visibility from 'visibilityjs';
import { mapActions } from 'vuex';
-
import {
GlButtonGroup,
GlButton,
@@ -10,6 +9,9 @@ import {
GlNewDropdownDivider,
GlTooltipDirective,
} from '@gitlab/ui';
+import { n__, __ } from '~/locale';
+
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const makeInterval = (length = 0, unit = 's') => {
const shortLabel = `${length}${unit}`;
@@ -53,6 +55,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
data() {
return {
refreshInterval: null,
@@ -60,6 +63,12 @@ export default {
};
},
computed: {
+ disableMetricDashboardRefreshRate() {
+ // Can refresh rates impact performance?
+ // Add "negative" feature flag called `disable_metric_dashboard_refresh_rate`
+ // See more at: https://gitlab.com/gitlab-org/gitlab/-/issues/229831
+ return this.glFeatures.disableMetricDashboardRefreshRate;
+ },
dropdownText() {
return this.refreshInterval?.shortLabel ?? __('Off');
},
@@ -90,7 +99,8 @@ export default {
};
this.stopAutoRefresh();
- if (document.hidden) {
+
+ if (Visibility.hidden()) {
// Inactive tab? Skip fetch and schedule again
schedule();
} else {
@@ -142,7 +152,12 @@ export default {
icon="retry"
@click="refresh"
/>
- <gl-new-dropdown v-gl-tooltip :title="s__('Metrics|Set refresh rate')" :text="dropdownText">
+ <gl-new-dropdown
+ v-if="!disableMetricDashboardRefreshRate"
+ v-gl-tooltip
+ :title="s__('Metrics|Set refresh rate')"
+ :text="dropdownText"
+ >
<gl-new-dropdown-item
:is-check-item="true"
:is-checked="refreshInterval === null"
diff --git a/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue
index 4e48292c48d..5563a27301d 100644
--- a/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue
+++ b/app/assets/javascripts/monitoring/components/variables/dropdown_field.vue
@@ -1,11 +1,11 @@
<script>
-import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlFormGroup, GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
export default {
components: {
GlFormGroup,
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
},
props: {
name: {
@@ -41,13 +41,16 @@ export default {
</script>
<template>
<gl-form-group :label="label">
- <gl-dropdown toggle-class="dropdown-menu-toggle" :text="text || s__('Metrics|Select a value')">
- <gl-dropdown-item
+ <gl-deprecated-dropdown
+ toggle-class="dropdown-menu-toggle"
+ :text="text || s__('Metrics|Select a value')"
+ >
+ <gl-deprecated-dropdown-item
v-for="val in options.values"
:key="val.value"
@click="onUpdate(val.value)"
- >{{ val.text }}</gl-dropdown-item
+ >{{ val.text }}</gl-deprecated-dropdown-item
>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index afeb3318eb9..81ad3137b8b 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -87,6 +87,10 @@ export const panelTypes = {
*/
SINGLE_STAT: 'single-stat',
/**
+ * Gauge
+ */
+ GAUGE_CHART: 'gauge',
+ /**
* Heatmap
*/
HEATMAP: 'heatmap',
@@ -213,7 +217,7 @@ export const annotationsSymbolIcon = 'path://m5 229 5 8h-10z';
* This technical debt is being tracked here
* https://gitlab.com/gitlab-org/gitlab/-/issues/214671
*/
-export const DEFAULT_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
+export const OVERVIEW_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
/**
* GitLab provide metrics dashboards that are available to a user once
@@ -272,3 +276,8 @@ export const keyboardShortcutKeys = {
DOWNLOAD_CSV: 'd',
CHART_COPY: 'c',
};
+
+export const thresholdModeTypes = {
+ ABSOLUTE: 'absolute',
+ PERCENTAGE: 'percentage',
+};
diff --git a/app/assets/javascripts/monitoring/csv_export.js b/app/assets/javascripts/monitoring/csv_export.js
new file mode 100644
index 00000000000..734e8dc07a7
--- /dev/null
+++ b/app/assets/javascripts/monitoring/csv_export.js
@@ -0,0 +1,147 @@
+import { getSeriesLabel } from '~/helpers/monitor_helper';
+
+/**
+ * Returns a label for a header of the csv.
+ *
+ * Includes double quotes ("") in case the header includes commas or other separator.
+ *
+ * @param {String} axisLabel
+ * @param {String} metricLabel
+ * @param {Object} metricAttributes
+ */
+const csvHeader = (axisLabel, metricLabel, metricAttributes = {}) =>
+ `${axisLabel} > ${getSeriesLabel(metricLabel, metricAttributes)}`;
+
+/**
+ * Returns an array with the header labels given a list of metrics
+ *
+ * ```
+ * metrics = [
+ * {
+ * label: "..." // user-defined label
+ * result: [
+ * {
+ * metric: { ... } // metricAttributes
+ * },
+ * ...
+ * ]
+ * },
+ * ...
+ * ]
+ * ```
+ *
+ * When metrics have a `label` or `metricAttributes`, they are
+ * used to generate the column name.
+ *
+ * @param {String} axisLabel - Main label
+ * @param {Array} metrics - Metrics with results
+ */
+const csvMetricHeaders = (axisLabel, metrics) =>
+ metrics.flatMap(({ label, result }) =>
+ // The `metric` in a `result` is a map of `metricAttributes`
+ // contains key-values to identify the series, rename it
+ // here for clarity.
+ result.map(({ metric: metricAttributes }) => {
+ return csvHeader(axisLabel, label, metricAttributes);
+ }),
+ );
+
+/**
+ * Returns a (flat) array with all the values arrays in each
+ * metric and series
+ *
+ * ```
+ * metrics = [
+ * {
+ * result: [
+ * {
+ * values: [ ... ] // `values`
+ * },
+ * ...
+ * ]
+ * },
+ * ...
+ * ]
+ * ```
+ *
+ * @param {Array} metrics - Metrics with results
+ */
+const csvMetricValues = metrics =>
+ metrics.flatMap(({ result }) => result.map(res => res.values || []));
+
+/**
+ * Returns headers and rows for csv, sorted by their timestamp.
+ *
+ * {
+ * headers: ["timestamp", "<col_1_name>", "col_2_name"],
+ * rows: [
+ * [ <timestamp>, <col_1_value>, <col_2_value> ],
+ * [ <timestamp>, <col_1_value>, <col_2_value> ]
+ * ...
+ * ]
+ * }
+ *
+ * @param {Array} metricHeaders
+ * @param {Array} metricValues
+ */
+const csvData = (metricHeaders, metricValues) => {
+ const rowsByTimestamp = {};
+
+ metricValues.forEach((values, colIndex) => {
+ values.forEach(([timestamp, value]) => {
+ if (!rowsByTimestamp[timestamp]) {
+ rowsByTimestamp[timestamp] = [];
+ }
+ // `value` should be in the right column
+ rowsByTimestamp[timestamp][colIndex] = value;
+ });
+ });
+
+ const rows = Object.keys(rowsByTimestamp)
+ .sort()
+ .map(timestamp => {
+ // force each row to have the same number of entries
+ rowsByTimestamp[timestamp].length = metricHeaders.length;
+ // add timestamp as the first entry
+ return [timestamp, ...rowsByTimestamp[timestamp]];
+ });
+
+ // Escape double quotes and enclose headers:
+ // "If double-quotes are used to enclose fields, then a double-quote
+ // appearing inside a field must be escaped by preceding it with
+ // another double quote."
+ // https://tools.ietf.org/html/rfc4180#page-2
+ const headers = metricHeaders.map(header => `"${header.replace(/"/g, '""')}"`);
+
+ return {
+ headers: ['timestamp', ...headers],
+ rows,
+ };
+};
+
+/**
+ * Returns dashboard panel's data in a string in CSV format
+ *
+ * @param {Object} graphData - Panel contents
+ * @returns {String}
+ */
+// eslint-disable-next-line import/prefer-default-export
+export const graphDataToCsv = graphData => {
+ const delimiter = ',';
+ const br = '\r\n';
+ const { metrics = [], y_label: axisLabel } = graphData;
+
+ const metricsWithResults = metrics.filter(metric => metric.result);
+ const metricHeaders = csvMetricHeaders(axisLabel, metricsWithResults);
+ const metricValues = csvMetricValues(metricsWithResults);
+ const { headers, rows } = csvData(metricHeaders, metricValues);
+
+ if (rows.length === 0) {
+ return '';
+ }
+
+ const headerLine = headers.join(delimiter) + br;
+ const lines = rows.map(row => row.join(delimiter));
+
+ return headerLine + lines.join(br) + br;
+};
diff --git a/app/assets/javascripts/monitoring/pages/panel_new_page.vue b/app/assets/javascripts/monitoring/pages/panel_new_page.vue
new file mode 100644
index 00000000000..8ff6adb47ca
--- /dev/null
+++ b/app/assets/javascripts/monitoring/pages/panel_new_page.vue
@@ -0,0 +1,45 @@
+<script>
+import { mapState } from 'vuex';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { DASHBOARD_PAGE } from '../router/constants';
+import DashboardPanelBuilder from '../components/dashboard_panel_builder.vue';
+
+export default {
+ components: {
+ GlButton,
+ DashboardPanelBuilder,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['panelPreviewYml']),
+ dashboardPageLocation() {
+ return {
+ ...this.$route,
+ name: DASHBOARD_PAGE,
+ };
+ },
+ },
+ i18n: {
+ backToDashboard: s__('Metrics|Back to dashboard'),
+ },
+};
+</script>
+<template>
+ <div class="gl-mt-5">
+ <div class="gl-display-flex gl-align-items-baseline gl-mb-5">
+ <gl-button
+ v-gl-tooltip
+ icon="go-back"
+ :to="dashboardPageLocation"
+ :aria-label="$options.i18n.backToDashboard"
+ :title="$options.i18n.backToDashboard"
+ class="gl-mr-5"
+ />
+ <h1 class="gl-font-size-h1 gl-my-0">{{ s__('Metrics|Add panel') }}</h1>
+ </div>
+ <dashboard-panel-builder />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql b/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql
index 27b49860b8a..32b982ff195 100644
--- a/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql
+++ b/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql
@@ -5,6 +5,7 @@ query getAnnotations(
$startingFrom: Time!
) {
project(fullPath: $projectPath) {
+ id
environments(name: $environmentName) {
nodes {
id
diff --git a/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql b/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql
index 17cd1b2c342..48d0a780fc7 100644
--- a/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql
+++ b/app/assets/javascripts/monitoring/queries/getEnvironments.query.graphql
@@ -1,5 +1,6 @@
query getEnvironments($projectPath: ID!, $search: String, $states: [String!]) {
project(fullPath: $projectPath) {
+ id
data: environments(search: $search, states: $states) {
environments: nodes {
name
diff --git a/app/assets/javascripts/monitoring/requests/index.js b/app/assets/javascripts/monitoring/requests/index.js
new file mode 100644
index 00000000000..28064361768
--- /dev/null
+++ b/app/assets/javascripts/monitoring/requests/index.js
@@ -0,0 +1,46 @@
+import axios from '~/lib/utils/axios_utils';
+import statusCodes from '~/lib/utils/http_status';
+import { backOff } from '~/lib/utils/common_utils';
+import { PROMETHEUS_TIMEOUT } from '../constants';
+
+const cancellableBackOffRequest = makeRequestCallback =>
+ backOff((next, stop) => {
+ makeRequestCallback()
+ .then(resp => {
+ if (resp.status === statusCodes.NO_CONTENT) {
+ next();
+ } else {
+ stop(resp);
+ }
+ })
+ // If the request is cancelled by axios
+ // then consider it as noop so that its not
+ // caught by subsequent catches
+ .catch(thrown => (axios.isCancel(thrown) ? undefined : stop(thrown)));
+ }, PROMETHEUS_TIMEOUT);
+
+export const getDashboard = (dashboardEndpoint, params) =>
+ cancellableBackOffRequest(() => axios.get(dashboardEndpoint, { params })).then(
+ axiosResponse => axiosResponse.data,
+ );
+
+export const getPrometheusQueryData = (prometheusEndpoint, params, opts) =>
+ cancellableBackOffRequest(() => axios.get(prometheusEndpoint, { params, ...opts }))
+ .then(axiosResponse => axiosResponse.data)
+ .then(prometheusResponse => prometheusResponse.data)
+ .catch(error => {
+ // Prometheus returns errors in specific cases
+ // https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview
+ const { response = {} } = error;
+ if (
+ response.status === statusCodes.BAD_REQUEST ||
+ response.status === statusCodes.UNPROCESSABLE_ENTITY ||
+ response.status === statusCodes.SERVICE_UNAVAILABLE
+ ) {
+ const { data } = response;
+ if (data?.status === 'error' && data?.error) {
+ throw new Error(data.error);
+ }
+ }
+ throw error;
+ });
diff --git a/app/assets/javascripts/monitoring/router/constants.js b/app/assets/javascripts/monitoring/router/constants.js
index fedfebe33e9..7834c14a65d 100644
--- a/app/assets/javascripts/monitoring/router/constants.js
+++ b/app/assets/javascripts/monitoring/router/constants.js
@@ -1,4 +1,7 @@
-export const BASE_DASHBOARD_PAGE = 'dashboard';
-export const CUSTOM_DASHBOARD_PAGE = 'custom_dashboard';
+export const DASHBOARD_PAGE = 'dashboard';
+export const PANEL_NEW_PAGE = 'panel_new';
-export default {};
+export default {
+ DASHBOARD_PAGE,
+ PANEL_NEW_PAGE,
+};
diff --git a/app/assets/javascripts/monitoring/router/routes.js b/app/assets/javascripts/monitoring/router/routes.js
index 4b82791178a..cc43fd8622a 100644
--- a/app/assets/javascripts/monitoring/router/routes.js
+++ b/app/assets/javascripts/monitoring/router/routes.js
@@ -1,6 +1,7 @@
import DashboardPage from '../pages/dashboard_page.vue';
+import PanelNewPage from '../pages/panel_new_page.vue';
-import { BASE_DASHBOARD_PAGE, CUSTOM_DASHBOARD_PAGE } from './constants';
+import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from './constants';
/**
* Because the cluster health page uses the dashboard
@@ -11,13 +12,13 @@ import { BASE_DASHBOARD_PAGE, CUSTOM_DASHBOARD_PAGE } from './constants';
*/
export default [
{
- name: BASE_DASHBOARD_PAGE,
- path: '/',
- component: DashboardPage,
+ name: PANEL_NEW_PAGE,
+ path: '/:dashboard(.+)?/panel/new',
+ component: PanelNewPage,
},
{
- name: CUSTOM_DASHBOARD_PAGE,
- path: '/:dashboard(.*)',
+ name: DASHBOARD_PAGE,
+ path: '/:dashboard(.+)?',
component: DashboardPage,
},
];
diff --git a/app/assets/javascripts/monitoring/services/alerts_service.js b/app/assets/javascripts/monitoring/services/alerts_service.js
index 4b7337972fe..a67675f1a3d 100644
--- a/app/assets/javascripts/monitoring/services/alerts_service.js
+++ b/app/assets/javascripts/monitoring/services/alerts_service.js
@@ -1,28 +1,39 @@
import axios from '~/lib/utils/axios_utils';
+const mapAlert = ({ runbook_url, ...alert }) => {
+ return { runbookUrl: runbook_url, ...alert };
+};
+
export default class AlertsService {
constructor({ alertsEndpoint }) {
this.alertsEndpoint = alertsEndpoint;
}
getAlerts() {
- return axios.get(this.alertsEndpoint).then(resp => resp.data);
+ return axios.get(this.alertsEndpoint).then(resp => mapAlert(resp.data));
}
- createAlert({ prometheus_metric_id, operator, threshold }) {
+ createAlert({ prometheus_metric_id, operator, threshold, runbookUrl }) {
return axios
- .post(this.alertsEndpoint, { prometheus_metric_id, operator, threshold })
- .then(resp => resp.data);
+ .post(this.alertsEndpoint, {
+ prometheus_metric_id,
+ operator,
+ threshold,
+ runbook_url: runbookUrl,
+ })
+ .then(resp => mapAlert(resp.data));
}
// eslint-disable-next-line class-methods-use-this
readAlert(alertPath) {
- return axios.get(alertPath).then(resp => resp.data);
+ return axios.get(alertPath).then(resp => mapAlert(resp.data));
}
// eslint-disable-next-line class-methods-use-this
- updateAlert(alertPath, { operator, threshold }) {
- return axios.put(alertPath, { operator, threshold }).then(resp => resp.data);
+ updateAlert(alertPath, { operator, threshold, runbookUrl }) {
+ return axios
+ .put(alertPath, { operator, threshold, runbook_url: runbookUrl })
+ .then(resp => mapAlert(resp.data));
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index a441882a47d..16a685305dc 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/browser';
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import {
gqClient,
@@ -13,16 +13,14 @@ import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
import getAnnotations from '../queries/getAnnotations.query.graphql';
import getDashboardValidationWarnings from '../queries/getDashboardValidationWarnings.query.graphql';
-import statusCodes from '../../lib/utils/http_status';
-import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
+import { convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
+import { getDashboard, getPrometheusQueryData } from '../requests';
-import {
- PROMETHEUS_TIMEOUT,
- ENVIRONMENT_AVAILABLE_STATE,
- DEFAULT_DASHBOARD_PATH,
- VARIABLE_TYPES,
-} from '../constants';
+import { ENVIRONMENT_AVAILABLE_STATE, OVERVIEW_DASHBOARD_PATH, VARIABLE_TYPES } from '../constants';
+
+const axiosCancelToken = axios.CancelToken;
+let cancelTokenSource;
function prometheusMetricQueryParams(timeRange) {
const { start, end } = convertToFixedRange(timeRange);
@@ -38,29 +36,18 @@ function prometheusMetricQueryParams(timeRange) {
};
}
-function backOffRequest(makeRequestCallback) {
- return backOff((next, stop) => {
- makeRequestCallback()
- .then(resp => {
- if (resp.status === statusCodes.NO_CONTENT) {
- next();
- } else {
- stop(resp);
- }
- })
- .catch(stop);
- }, PROMETHEUS_TIMEOUT);
-}
-
-function getPrometheusQueryData(prometheusEndpoint, params) {
- return backOffRequest(() => axios.get(prometheusEndpoint, { params }))
- .then(res => res.data)
- .then(response => {
- if (response.status === 'error') {
- throw new Error(response.error);
- }
- return response.data;
- });
+/**
+ * Extract error messages from API or HTTP request errors.
+ *
+ * - API errors are in `error.response.data.message`
+ * - HTTP (axios) errors are in `error.messsage`
+ *
+ * @param {Object} error
+ * @returns {String} User friendly error message
+ */
+function extractErrorMessage(error) {
+ const message = error?.response?.data?.message;
+ return message ?? error.message;
}
// Setup
@@ -126,8 +113,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
params.dashboard = getters.fullDashboardPath;
}
- return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
- .then(resp => resp.data)
+ return getDashboard(state.dashboardEndpoint, params)
.then(response => {
dispatch('receiveMetricsDashboardSuccess', { response });
/**
@@ -329,7 +315,7 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
export const fetchAnnotations = ({ state, dispatch, getters }) => {
const { start } = convertToFixedRange(state.timeRange);
- const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
+ const dashboardPath = getters.fullDashboardPath || OVERVIEW_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getAnnotations,
@@ -362,12 +348,12 @@ export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_AN
export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) => {
/**
- * Normally, the default dashboard won't throw any validation warnings.
+ * Normally, the overview dashboard won't throw any validation warnings.
*
- * However, if a bug sneaks into the default dashboard making it invalid,
+ * However, if a bug sneaks into the overview dashboard making it invalid,
* this might come handy for our clients
*/
- const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
+ const dashboardPath = getters.fullDashboardPath || OVERVIEW_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getDashboardValidationWarnings,
@@ -484,12 +470,10 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
if (variable.type === VARIABLE_TYPES.metric_label_values) {
const { prometheusEndpointPath, label } = variable.options;
- const optionsRequest = backOffRequest(() =>
- axios.get(prometheusEndpointPath, {
- params: { start_time, end_time },
- }),
- )
- .then(({ data }) => data.data)
+ const optionsRequest = getPrometheusQueryData(prometheusEndpointPath, {
+ start_time,
+ end_time,
+ })
.then(data => {
commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data });
})
@@ -507,5 +491,59 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
return Promise.all(optionsRequests);
};
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
+// Panel Builder
+
+export const setPanelPreviewTimeRange = ({ commit }, timeRange) => {
+ commit(types.SET_PANEL_PREVIEW_TIME_RANGE, timeRange);
+};
+
+export const fetchPanelPreview = ({ state, commit, dispatch }, panelPreviewYml) => {
+ if (!panelPreviewYml) {
+ return null;
+ }
+
+ commit(types.SET_PANEL_PREVIEW_IS_SHOWN, true);
+ commit(types.REQUEST_PANEL_PREVIEW, panelPreviewYml);
+
+ return axios
+ .post(state.panelPreviewEndpoint, { panel_yaml: panelPreviewYml })
+ .then(({ data }) => {
+ commit(types.RECEIVE_PANEL_PREVIEW_SUCCESS, data);
+
+ dispatch('fetchPanelPreviewMetrics');
+ })
+ .catch(error => {
+ commit(types.RECEIVE_PANEL_PREVIEW_FAILURE, extractErrorMessage(error));
+ });
+};
+
+export const fetchPanelPreviewMetrics = ({ state, commit }) => {
+ if (cancelTokenSource) {
+ cancelTokenSource.cancel();
+ }
+ cancelTokenSource = axiosCancelToken.source();
+
+ const defaultQueryParams = prometheusMetricQueryParams(state.panelPreviewTimeRange);
+
+ state.panelPreviewGraphData.metrics.forEach((metric, index) => {
+ commit(types.REQUEST_PANEL_PREVIEW_METRIC_RESULT, { index });
+
+ const params = { ...defaultQueryParams };
+ if (metric.step) {
+ params.step = metric.step;
+ }
+ return getPrometheusQueryData(metric.prometheusEndpointPath, params, {
+ cancelToken: cancelTokenSource.token,
+ })
+ .then(data => {
+ commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS, { index, data });
+ })
+ .catch(error => {
+ 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
+ throw error;
+ });
+ });
+};
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/actions.js b/app/assets/javascripts/monitoring/stores/embed_group/actions.js
index cbe0950d954..4a7572bdbd9 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/actions.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/actions.js
@@ -1,5 +1,4 @@
import * as types from './mutation_types';
+// eslint-disable-next-line import/prefer-default-export
export const addModule = ({ commit }, data) => commit(types.ADD_MODULE, data);
-
-export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/getters.js b/app/assets/javascripts/monitoring/stores/embed_group/getters.js
index 9b08cf762c1..096d8d03096 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/getters.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/getters.js
@@ -1,4 +1,3 @@
+// eslint-disable-next-line import/prefer-default-export
export const metricsWithData = (state, getters, rootState, rootGetters) =>
state.modules.map(module => rootGetters[`${module}/metricsWithData`]().length);
-
-export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
index e7a425d3623..7fd3f0f8647 100644
--- a/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/embed_group/mutation_types.js
@@ -1,3 +1,2 @@
+// eslint-disable-next-line import/prefer-default-export
export const ADD_MODULE = 'ADD_MODULE';
-
-export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index 3aa711a0509..8ed83cf02fe 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -170,6 +170,3 @@ export const getCustomVariablesParams = state =>
*/
export const fullDashboardPath = state =>
normalizeCustomDashboardPath(state.currentDashboard, state.customDashboardBasePath);
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index d408628fc4d..1d7279912cc 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -46,3 +46,17 @@ export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS';
export const SET_ENVIRONMENTS_FILTER = 'SET_ENVIRONMENTS_FILTER';
export const SET_EXPANDED_PANEL = 'SET_EXPANDED_PANEL';
+
+// Panel preview
+export const REQUEST_PANEL_PREVIEW = 'REQUEST_PANEL_PREVIEW';
+export const RECEIVE_PANEL_PREVIEW_SUCCESS = 'RECEIVE_PANEL_PREVIEW_SUCCESS';
+export const RECEIVE_PANEL_PREVIEW_FAILURE = 'RECEIVE_PANEL_PREVIEW_FAILURE';
+
+export const REQUEST_PANEL_PREVIEW_METRIC_RESULT = 'REQUEST_PANEL_PREVIEW_METRIC_RESULT';
+export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS =
+ 'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS';
+export const RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE =
+ 'RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE';
+
+export const SET_PANEL_PREVIEW_TIME_RANGE = 'SET_PANEL_PREVIEW_TIME_RANGE';
+export const SET_PANEL_PREVIEW_IS_SHOWN = 'SET_PANEL_PREVIEW_IS_SHOWN';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 744441c8935..09a5861b475 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import { pick } from 'lodash';
import * as types from './mutation_types';
-import { mapToDashboardViewModel, normalizeQueryResponseData } from './utils';
+import { mapToDashboardViewModel, mapPanelToViewModel, normalizeQueryResponseData } from './utils';
import httpStatusCodes from '~/lib/utils/http_status';
-import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
+import { BACKOFF_TIMEOUT } from '~/lib/utils/common_utils';
import { dashboardEmptyStates, endpointKeys, initialStateKeys, metricStates } from '../constants';
import { optionsFromSeriesData } from './variable_mapping';
@@ -53,6 +53,14 @@ const emptyStateFromError = error => {
return metricStates.UNKNOWN_ERROR;
};
+export const metricStateFromData = data => {
+ if (data?.result?.length) {
+ const result = normalizeQueryResponseData(data);
+ return { state: metricStates.OK, result: Object.freeze(result) };
+ }
+ return { state: metricStates.NO_DATA, result: null };
+};
+
export default {
/**
* Dashboard panels structure and global state
@@ -154,17 +162,11 @@ export default {
},
[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, data }) {
const metric = findMetricInDashboard(metricId, state.dashboard);
- metric.loading = false;
+ const metricState = metricStateFromData(data);
- if (!data.result || data.result.length === 0) {
- metric.state = metricStates.NO_DATA;
- metric.result = null;
- } else {
- const result = normalizeQueryResponseData(data);
-
- metric.state = metricStates.OK;
- metric.result = Object.freeze(result);
- }
+ metric.loading = false;
+ metric.state = metricState.state;
+ metric.result = metricState.result;
},
[types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) {
const metric = findMetricInDashboard(metricId, state.dashboard);
@@ -218,4 +220,54 @@ export default {
// Add new options with assign to ensure Vue reactivity
Object.assign(variable.options, { values });
},
+
+ [types.REQUEST_PANEL_PREVIEW](state, panelPreviewYml) {
+ state.panelPreviewIsLoading = true;
+
+ state.panelPreviewYml = panelPreviewYml;
+ state.panelPreviewGraphData = null;
+ state.panelPreviewError = null;
+ },
+ [types.RECEIVE_PANEL_PREVIEW_SUCCESS](state, payload) {
+ state.panelPreviewIsLoading = false;
+
+ state.panelPreviewGraphData = mapPanelToViewModel(payload);
+ state.panelPreviewError = null;
+ },
+ [types.RECEIVE_PANEL_PREVIEW_FAILURE](state, error) {
+ state.panelPreviewIsLoading = false;
+
+ state.panelPreviewGraphData = null;
+ state.panelPreviewError = error;
+ },
+
+ [types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](state, { index }) {
+ const metric = state.panelPreviewGraphData.metrics[index];
+
+ metric.loading = true;
+ if (!metric.result) {
+ metric.state = metricStates.LOADING;
+ }
+ },
+ [types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS](state, { index, data }) {
+ const metric = state.panelPreviewGraphData.metrics[index];
+ const metricState = metricStateFromData(data);
+
+ metric.loading = false;
+ metric.state = metricState.state;
+ metric.result = metricState.result;
+ },
+ [types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](state, { index, error }) {
+ const metric = state.panelPreviewGraphData.metrics[index];
+
+ metric.loading = false;
+ metric.state = emptyStateFromError(error);
+ metric.result = null;
+ },
+ [types.SET_PANEL_PREVIEW_TIME_RANGE](state, timeRange) {
+ state.panelPreviewTimeRange = timeRange;
+ },
+ [types.SET_PANEL_PREVIEW_IS_SHOWN](state, isPreviewShown) {
+ state.panelPreviewIsShown = isPreviewShown;
+ },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index 89738756ffe..ef8b1adb624 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -1,12 +1,14 @@
import invalidUrl from '~/lib/utils/invalid_url';
import { timezones } from '../format_date';
import { dashboardEmptyStates } from '../constants';
+import { defaultTimeRange } from '~/vue_shared/constants';
export default () => ({
// API endpoints
deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl,
dashboardsEndpoint: invalidUrl,
+ panelPreviewEndpoint: invalidUrl,
// Dashboard request parameters
timeRange: null,
@@ -59,6 +61,15 @@ export default () => ({
* via the dashboard yml file.
*/
links: [],
+
+ // Panel editor / builder
+ panelPreviewYml: '',
+ panelPreviewIsLoading: false,
+ panelPreviewGraphData: null,
+ panelPreviewError: null,
+ panelPreviewTimeRange: defaultTimeRange,
+ panelPreviewIsShown: false,
+
// Other project data
dashboardTimezone: timezones.LOCAL,
annotations: [],
@@ -69,9 +80,11 @@ export default () => ({
currentEnvironmentName: null,
// GitLab paths to other pages
+ externalDashboardUrl: '',
projectPath: null,
operationsSettingsPath: '',
logsPath: invalidUrl,
+ addDashboardDocumentationPath: '',
// static paths
customDashboardBasePath: '',
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 51562593ee8..df7f22e622f 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -176,7 +176,11 @@ export const mapPanelToViewModel = ({
field,
metrics = [],
links = [],
+ min_value,
max_value,
+ split,
+ thresholds,
+ format,
}) => {
// Both `x_axis.name` and `x_label` are supported for now
// https://gitlab.com/gitlab-org/gitlab/issues/210521
@@ -195,7 +199,11 @@ export const mapPanelToViewModel = ({
yAxis,
xAxis,
field,
+ minValue: min_value,
maxValue: max_value,
+ split,
+ thresholds,
+ format,
links: links.map(mapLinksToViewModel),
metrics: mapToMetricsViewModel(metrics),
};
@@ -465,9 +473,9 @@ export const addPrefixToCustomVariableParams = name => `variables[${name}]`;
* metrics dashboard to work with custom dashboard file names instead
* of the entire path.
*
- * If dashboard is empty, it is the default dashboard.
+ * If dashboard is empty, it is the overview dashboard.
* If dashboard is set, it usually is a custom dashboard unless
- * explicitly it is set to default dashboard path.
+ * explicitly it is set to overview dashboard path.
*
* @param {String} dashboard dashboard path
* @param {String} dashboardPrefix custom dashboard directory prefix
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 0c6fcad9dd0..92bbce498d5 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -24,13 +24,16 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
deploymentsEndpoint,
dashboardEndpoint,
dashboardsEndpoint,
+ panelPreviewEndpoint,
dashboardTimezone,
canAccessOperationsSettings,
operationsSettingsPath,
projectPath,
logsPath,
+ externalDashboardUrl,
currentEnvironmentName,
customDashboardBasePath,
+ addDashboardDocumentationPath,
...dataProps
} = dataset;
@@ -45,13 +48,16 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
deploymentsEndpoint,
dashboardEndpoint,
dashboardsEndpoint,
+ panelPreviewEndpoint,
dashboardTimezone,
canAccessOperationsSettings,
operationsSettingsPath,
projectPath,
logsPath,
+ externalDashboardUrl,
currentEnvironmentName,
customDashboardBasePath,
+ addDashboardDocumentationPath,
},
dataProps,
};
diff --git a/app/assets/javascripts/monitoring/validators.js b/app/assets/javascripts/monitoring/validators.js
index cd426f1a221..c6b323f6360 100644
--- a/app/assets/javascripts/monitoring/validators.js
+++ b/app/assets/javascripts/monitoring/validators.js
@@ -1,3 +1,12 @@
+import { isSafeURL } from '~/lib/utils/url_utility';
+
+const isRunbookUrlValid = runbookUrl => {
+ if (!runbookUrl) {
+ return true;
+ }
+ return isSafeURL(runbookUrl);
+};
+
// Prop validator for alert information, expecting an object like the example below.
//
// {
@@ -8,6 +17,7 @@
// query: "rate(http_requests_total[5m])[30m:1m]",
// threshold: 0.002,
// title: "Core Usage (Total)",
+// runbookUrl: "https://www.gitlab.com/my-project/-/wikis/runbook"
// }
// }
export function alertsValidator(value) {
@@ -19,7 +29,8 @@ export function alertsValidator(value) {
alert.metricId &&
typeof alert.metricId === 'string' &&
alert.operator &&
- typeof alert.threshold === 'number'
+ typeof alert.threshold === 'number' &&
+ isRunbookUrlValid(alert.runbookUrl)
);
});
}
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index fcde9bf7849..2be7cc951fc 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -3,8 +3,9 @@ import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import store from '~/mr_notes/stores';
import notesApp from '../notes/components/notes_app.vue';
-import discussionKeyboardNavigator from '../notes/components/discussion_keyboard_navigator.vue';
+import discussionNavigator from '../notes/components/discussion_navigator.vue';
import initWidget from '../vue_merge_request_widget';
+import { parseBoolean } from '~/lib/utils/common_utils';
export default () => {
// eslint-disable-next-line no-new
@@ -20,6 +21,7 @@ export default () => {
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
noteableData.targetType = notesDataset.targetType;
+ noteableData.discussion_locked = parseBoolean(notesDataset.isLocked);
return {
noteableData,
@@ -69,11 +71,11 @@ export default () => {
},
},
render(createElement) {
- // NOTE: Even though `discussionKeyboardNavigator` is added to the `notes-app`,
+ // NOTE: Even though `discussionNavigator` is added to the `notes-app`,
// it adds a global key listener so it works on the diffs tab as well.
// If we create a single Vue app for all of the MR tabs, we should move this
// up the tree, to the root.
- return createElement(discussionKeyboardNavigator, [
+ return createElement(discussionNavigator, [
createElement('notes-app', {
props: {
noteableData: this.noteableData,
diff --git a/app/assets/javascripts/mr_notes/stores/getters.js b/app/assets/javascripts/mr_notes/stores/getters.js
index e48cfcd9564..245443d7ecf 100644
--- a/app/assets/javascripts/mr_notes/stores/getters.js
+++ b/app/assets/javascripts/mr_notes/stores/getters.js
@@ -1,3 +1,8 @@
+// Note: this getter is important because
+// `noteableData` is namespaced under `notes` for `~/mr_notes/stores`
+// while `noteableData` is directly available as `state.noteableData` for `~/notes/stores`
+export const getNoteableData = state => state.notes.noteableData;
+
export default {
isLoggedIn(state, getters) {
return Boolean(getters.getUserData.id);
diff --git a/app/assets/javascripts/mr_tabs_popover/components/popover.vue b/app/assets/javascripts/mr_tabs_popover/components/popover.vue
deleted file mode 100644
index 30455709149..00000000000
--- a/app/assets/javascripts/mr_tabs_popover/components/popover.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<script>
-import { GlPopover, GlDeprecatedButton, GlLink } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
-import axios from '~/lib/utils/axios_utils';
-
-export default {
- components: {
- GlPopover,
- GlDeprecatedButton,
- GlLink,
- Icon,
- },
- props: {
- dismissEndpoint: {
- type: String,
- required: true,
- },
- featureId: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- showPopover: false,
- };
- },
- mounted() {
- setTimeout(() => {
- this.showPopover = true;
- }, 2000);
- },
- methods: {
- onDismiss() {
- this.showPopover = false;
-
- axios.post(this.dismissEndpoint, {
- feature_name: this.featureId,
- });
- },
- },
-};
-</script>
-
-<template>
- <gl-popover target="#diffs-tab" placement="bottom" :show="showPopover">
- <p class="mb-2">
- {{
- __(
- 'Now you can access the merge request navigation tabs at the top, where they’re easier to find.',
- )
- }}
- </p>
- <p>
- <gl-link href="https://gitlab.com/gitlab-org/gitlab/issues/36125" target="_blank">
- {{ __('More information and share feedback') }}
- <icon name="external-link" :size="10" />
- </gl-link>
- </p>
- <gl-deprecated-button
- variant="primary"
- size="sm"
- data-qa-selector="dismiss_popover_button"
- @click="onDismiss"
- >
- {{ __('Got it') }}
- </gl-deprecated-button>
- </gl-popover>
-</template>
diff --git a/app/assets/javascripts/mr_tabs_popover/index.js b/app/assets/javascripts/mr_tabs_popover/index.js
deleted file mode 100644
index 9ee0ba046f0..00000000000
--- a/app/assets/javascripts/mr_tabs_popover/index.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import Vue from 'vue';
-import Popover from './components/popover.vue';
-
-export default el =>
- new Vue({
- el,
- render(createElement) {
- return createElement(Popover, {
- props: { dismissEndpoint: el.dataset.dismissEndpoint, featureId: el.dataset.featureId },
- });
- },
- });
diff --git a/app/assets/javascripts/namespaces/leave_by_url.js b/app/assets/javascripts/namespaces/leave_by_url.js
index b817d38960c..bf77617d516 100644
--- a/app/assets/javascripts/namespaces/leave_by_url.js
+++ b/app/assets/javascripts/namespaces/leave_by_url.js
@@ -1,4 +1,4 @@
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { __, sprintf } from '~/locale';
import { getParameterByName } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 3cc95168ba1..3ea597a08d3 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -40,9 +40,6 @@ export default class BranchGraph {
}
prepareData(days, commits) {
- let c = 0;
- let j = 0;
- let len = 0;
this.days = days;
this.commits = commits;
this.collectParents();
@@ -53,38 +50,33 @@ export default class BranchGraph {
this.r = Raphael(this.element.get(0), cw, ch);
this.top = this.r.set();
this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
- const ref = this.commits;
- for (j = 0, len = ref.length; j < len; j += 1) {
- c = ref[j];
- if (c.id in this.parents) {
- c.isParent = true;
+ this.commits = this.commits.reduce((acc, commit) => {
+ const updatedCommit = commit;
+ if (commit.id in this.parents) {
+ updatedCommit.isParent = true;
}
- this.preparedCommits[c.id] = c;
- this.markCommit(c);
- }
+ acc.push(updatedCommit);
+ this.preparedCommits[commit.id] = commit;
+ this.markCommit(commit);
+ return acc;
+ }, []);
return this.collectColors();
}
collectParents() {
- let j = 0;
- let l = 0;
- let len = 0;
- let len1 = 0;
const ref = this.commits;
const results = [];
- for (j = 0, len = ref.length; j < len; j += 1) {
- const c = ref[j];
+ ref.forEach(c => {
this.mtime = Math.max(this.mtime, c.time);
this.mspace = Math.max(this.mspace, c.space);
const ref1 = c.parents;
const results1 = [];
- for (l = 0, len1 = ref1.length; l < len1; l += 1) {
- const p = ref1[l];
+ ref1.forEach(p => {
this.parents[p[0]] = true;
results1.push((this.mspace = Math.max(this.mspace, p[1])));
- }
+ });
results.push(results1);
- }
+ });
return results;
}
@@ -114,7 +106,6 @@ export default class BranchGraph {
fill: '#444',
});
const ref = this.days;
-
for (mm = 0, len = ref.length; mm < len; mm += 1) {
const day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
@@ -295,7 +286,6 @@ export default class BranchGraph {
const { r } = this;
const ref = commit.parents;
const results = [];
-
for (i = 0, len = ref.length; i < len; i += 1) {
const parent = ref[i];
const parentCommit = this.preparedCommits[parent[0]];
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index fcb09ea90db..fa1afdcd16f 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,6 +1,6 @@
<script>
import marked from 'marked';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import katex from 'katex';
import Prompt from './prompt.vue';
@@ -104,65 +104,58 @@ export default {
return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), {
// allowedTags from GitLab's inline HTML guidelines
// https://docs.gitlab.com/ee/user/markdown.html#inline-html
- allowedTags: [
+ ALLOWED_TAGS: [
+ 'a',
+ 'abbr',
+ 'b',
+ 'blockquote',
+ 'br',
+ 'code',
+ 'dd',
+ 'del',
+ 'div',
+ 'dl',
+ 'dt',
+ 'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
- 'h7',
- 'h8',
- 'br',
- 'b',
+ 'hr',
'i',
- 'strong',
- 'em',
- 'a',
- 'pre',
- 'code',
'img',
- 'tt',
- 'div',
'ins',
- 'del',
- 'sup',
- 'sub',
- 'p',
- 'ol',
- 'ul',
- 'table',
- 'thead',
- 'tbody',
- 'tfoot',
- 'blockquote',
- 'dl',
- 'dt',
- 'dd',
'kbd',
+ 'li',
+ 'ol',
+ 'p',
+ 'pre',
'q',
- 'samp',
- 'var',
- 'hr',
- 'ruby',
- 'rt',
'rp',
- 'li',
- 'tr',
- 'td',
- 'th',
+ 'rt',
+ 'ruby',
's',
- 'strike',
+ 'samp',
'span',
- 'abbr',
- 'abbr',
+ 'strike',
+ 'strong',
+ 'sub',
'summary',
+ 'sup',
+ 'table',
+ 'tbody',
+ 'td',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'tr',
+ 'tt',
+ 'ul',
+ 'var',
],
- allowedAttributes: {
- '*': ['class', 'style'],
- a: ['href'],
- img: ['src'],
- },
+ ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
});
},
},
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index 8dc2d73af9b..b36761993ea 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -1,5 +1,5 @@
<script>
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import Prompt from '../prompt.vue';
export default {
@@ -23,10 +23,7 @@ export default {
computed: {
sanitizedOutput() {
return sanitize(this.rawCode, {
- allowedTags: sanitize.defaults.allowedTags.concat(['img', 'svg']),
- allowedAttributes: {
- img: ['src'],
- },
+ ALLOWED_ATTR: ['src'],
});
},
showOutput() {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index f4982507adb..3940b4b4724 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -22,7 +22,7 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import syntaxHighlight from '~/syntax_highlight';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
-import Flash from './flash';
+import { deprecatedCreateFlash as Flash } from './flash';
import { defaultAutocompleteConfig } from './gfm_auto_complete';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
@@ -1336,11 +1336,12 @@ export default class Notes {
toggleCommitList(e) {
const $element = $(e.currentTarget);
const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
+ const $svgChevronUpElement = $element.find('svg.js-chevron-up');
+ const $svgChevronDownElement = $element.find('svg.js-chevron-down');
+
+ $svgChevronUpElement.toggleClass('gl-display-none');
+ $svgChevronDownElement.toggleClass('gl-display-none');
- $element
- .find('.fa')
- .toggleClass('fa-angle-down')
- .toggleClass('fa-angle-up');
$closestSystemCommitList.toggleClass('hide-shade');
}
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index ac93d3df654..7cfff98e9f7 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -6,7 +6,7 @@ import Autosize from 'autosize';
import { GlAlert, GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import Flash from '../../flash';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import Autosave from '../../autosave';
import {
capitalizeFirstCharacter,
diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue
index 251199f1778..878a748e99a 100644
--- a/app/assets/javascripts/notes/components/discussion_actions.vue
+++ b/app/assets/javascripts/notes/components/discussion_actions.vue
@@ -3,6 +3,7 @@ import ReplyPlaceholder from './discussion_reply_placeholder.vue';
import ResolveDiscussionButton from './discussion_resolve_button.vue';
import ResolveWithIssueButton from './discussion_resolve_with_issue_button.vue';
import JumpToNextDiscussionButton from './discussion_jump_to_next_button.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'DiscussionActions',
@@ -12,6 +13,7 @@ export default {
ResolveWithIssueButton,
JumpToNextDiscussionButton,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
discussion: {
type: Object,
@@ -36,6 +38,9 @@ export default {
},
},
computed: {
+ hideJumpToNextUnresolvedInThreads() {
+ return this.glFeatures.hideJumpToNextUnresolvedInThreads;
+ },
resolvableNotes() {
return this.discussion.notes.filter(x => x.resolvable);
},
@@ -70,7 +75,11 @@ export default {
/>
</div>
<div
- v-if="discussion.resolvable && shouldShowJumpToNextDiscussion"
+ v-if="
+ !hideJumpToNextUnresolvedInThreads &&
+ discussion.resolvable &&
+ shouldShowJumpToNextDiscussion
+ "
class="btn-group discussion-actions ml-sm-2"
>
<jump-to-next-discussion-button :from-discussion-id="discussion.id" />
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
index 25ff49fbd0f..8dc4b43d69a 100644
--- a/app/assets/javascripts/notes/components/discussion_filter_note.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
@@ -7,7 +7,7 @@ import notesEventHub from '../event_hub';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
Icon,
},
computed: {
@@ -40,12 +40,12 @@ export default {
<div class="timeline-content">
<div ref="timelineContent" v-html="timelineContent"></div>
<div class="discussion-filter-actions mt-2">
- <gl-deprecated-button ref="showAllActivity" variant="default" @click="selectFilter(0)">
+ <gl-button ref="showAllActivity" variant="default" @click="selectFilter(0)">
{{ __('Show all activity') }}
- </gl-deprecated-button>
- <gl-deprecated-button ref="showComments" variant="default" @click="selectFilter(1)">
+ </gl-button>
+ <gl-button ref="showComments" variant="default" @click="selectFilter(1)">
{{ __('Show comments only') }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</div>
</li>
diff --git a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue b/app/assets/javascripts/notes/components/discussion_navigator.vue
index 2dc222d08f9..facc53e27a6 100644
--- a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue
+++ b/app/assets/javascripts/notes/components/discussion_navigator.vue
@@ -2,9 +2,13 @@
/* global Mousetrap */
import 'mousetrap';
import discussionNavigation from '~/notes/mixins/discussion_navigation';
+import eventHub from '~/notes/event_hub';
export default {
mixins: [discussionNavigation],
+ created() {
+ eventHub.$on('jumpToFirstUnresolvedDiscussion', this.jumpToFirstUnresolvedDiscussion);
+ },
mounted() {
Mousetrap.bind('n', this.jumpToNextDiscussion);
Mousetrap.bind('p', this.jumpToPreviousDiscussion);
@@ -12,6 +16,8 @@ export default {
beforeDestroy() {
Mousetrap.unbind('n');
Mousetrap.unbind('p');
+
+ eventHub.$off('jumpToFirstUnresolvedDiscussion', this.jumpToFirstUnresolvedDiscussion);
},
render() {
return this.$slots.default;
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 458da5cf67f..a1e887c47d0 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -9,6 +9,7 @@ import NoteableNote from './noteable_note.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
import NoteEditedText from './note_edited_text.vue';
import DiscussionNotesRepliesWrapper from './discussion_notes_replies_wrapper.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'DiscussionNotes',
@@ -17,6 +18,7 @@ export default {
NoteEditedText,
DiscussionNotesRepliesWrapper,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
discussion: {
type: Object,
@@ -93,6 +95,18 @@ export default {
componentData(note) {
return note.isPlaceholderNote ? note.notes[0] : note;
},
+ handleMouseEnter(discussion) {
+ if (this.glFeatures.multilineComments && discussion.position) {
+ this.setSelectedCommentPositionHover(discussion.position.line_range);
+ }
+ },
+ handleMouseLeave(discussion) {
+ // Even though position isn't used here we still don't want to unecessarily call a mutation
+ // The lack of position tells us that highlighting is irrelevant in this context
+ if (this.glFeatures.multilineComments && discussion.position) {
+ this.setSelectedCommentPositionHover();
+ }
+ },
},
};
</script>
@@ -101,8 +115,8 @@ export default {
<div class="discussion-notes">
<ul
class="notes"
- @mouseenter="setSelectedCommentPositionHover(discussion.position.line_range)"
- @mouseleave="setSelectedCommentPositionHover()"
+ @mouseenter="handleMouseEnter(discussion)"
+ @mouseleave="handleMouseLeave(discussion)"
>
<template v-if="shouldGroupReplies">
<component
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
index e5f78b1c7de..cace382ccd6 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -1,12 +1,10 @@
<script>
-import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
-import Icon from '~/vue_shared/components/icon.vue';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
export default {
name: 'ResolveWithIssueButton',
components: {
- Icon,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -22,13 +20,12 @@ export default {
<template>
<div class="btn-group" role="group">
- <gl-deprecated-button
+ <gl-button
v-gl-tooltip
:href="url"
:title="s__('MergeRequests|Resolve this thread in a new issue')"
class="new-issue-for-discussion discussion-create-issue-btn"
- >
- <icon name="issue-new" />
- </gl-deprecated-button>
+ icon="issue-new"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 7615b0518b7..a8ae7fb48f0 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,13 +1,13 @@
<script>
-import { __ } from '~/locale';
import { mapGetters } from 'vuex';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
import Icon from '~/vue_shared/components/icon.vue';
import ReplyButton from './note_actions/reply_button.vue';
import eventHub from '~/sidebar/event_hub';
import Api from '~/api';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
export default {
name: 'NoteActions',
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 5181b5f26ee..cf3e991986c 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,7 +1,7 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import AwardsList from '~/vue_shared/components/awards_list.vue';
-import Flash from '../../flash';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import { __ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 81812ee2279..9ded5ab648e 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -191,7 +191,7 @@ export default {
name="eye-slash"
:size="14"
:title="s__('Notes|Private comments are accessible by internal staff only')"
- class="gl-ml-1 gl-text-gray-800 align-middle"
+ class="gl-ml-1 gl-text-gray-700 align-middle"
/>
<slot name="extra-controls"></slot>
<i
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 7fe50d36c0c..b4176c6063b 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -7,7 +7,7 @@ import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import icon from '~/vue_shared/components/icon.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import DraftNote from '~/batch_comments/components/draft_note.vue';
-import Flash from '../../flash';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import diffDiscussionHeader from './diff_discussion_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
@@ -149,9 +149,14 @@ export default {
'removePlaceholderNotes',
'toggleResolveNote',
'removeConvertedDiscussion',
+ 'expandDiscussion',
]),
showReplyForm() {
this.isReplying = true;
+
+ if (!this.discussion.expanded) {
+ this.expandDiscussion({ discussionId: this.discussion.id });
+ }
},
cancelReplyForm(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 9bf8cffe940..ce771e67cbb 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -7,7 +7,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { __, s__, sprintf } from '../../locale';
-import Flash from '../../flash';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue';
@@ -23,7 +23,6 @@ import {
commentLineOptions,
formatLineRange,
} from './multiline_comment_utils';
-import MultilineCommentForm from './multiline_comment_form.vue';
export default {
name: 'NoteableNote',
@@ -34,7 +33,6 @@ export default {
noteActions,
NoteBody,
TimelineEntryItem,
- MultilineCommentForm,
},
mixins: [noteable, resolvable, glFeatureFlagsMixin()],
props: {
@@ -147,14 +145,17 @@ export default {
return getEndLineNumber(this.lineRange);
},
showMultiLineComment() {
- if (!this.glFeatures.multilineComments || !this.discussionRoot) return false;
- if (this.isEditing) return true;
+ if (
+ !this.glFeatures.multilineComments ||
+ !this.discussionRoot ||
+ this.startLineNumber.length === 0 ||
+ this.endLineNumber.length === 0
+ )
+ return false;
return this.line && this.startLineNumber !== this.endLineNumber;
},
commentLineOptions() {
- if (!this.diffFile || !this.line) return [];
-
const sideA = this.line.type === 'new' ? 'right' : 'left';
const sideB = sideA === 'left' ? 'right' : 'left';
const lines = this.diffFile.highlighted_diff_lines.length
@@ -207,6 +208,7 @@ export default {
'scrollToNoteIfNeeded',
'updateAssignees',
'setSelectedCommentPositionHover',
+ 'updateDiscussionPosition',
]),
editHandler() {
this.isEditing = true;
@@ -249,8 +251,13 @@ export default {
...this.note.position,
};
- if (this.commentLineStart && this.line)
+ if (this.discussionRoot && this.commentLineStart && this.line) {
position.line_range = formatLineRange(this.commentLineStart, this.line);
+ this.updateDiscussionPosition({
+ discussionId: this.note.discussion_id,
+ position,
+ });
+ }
this.$emit('handleUpdateNote', {
note: this.note,
@@ -337,28 +344,19 @@ export default {
:data-note-id="note.id"
class="note note-wrapper qa-noteable-note-item"
>
- <div v-if="showMultiLineComment" data-testid="multiline-comment">
- <multiline-comment-form
- v-if="isEditing && note.position"
- v-model="commentLineStart"
- :line="line"
- :comment-line-options="commentLineOptions"
- :line-range="note.position.line_range"
- class="gl-mb-3 gl-text-gray-700 gl-pb-3"
- />
- <div
- v-else
- class="gl-mb-3 gl-text-gray-700 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3"
- >
- <gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
- <template #startLine>
- <span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
- </template>
- <template #endLine>
- <span :class="getLineClasses(endLineNumber)">{{ endLineNumber }}</span>
- </template>
- </gl-sprintf>
- </div>
+ <div
+ v-if="showMultiLineComment"
+ data-testid="multiline-comment"
+ class="gl-mb-3 gl-text-gray-500 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-pb-3"
+ >
+ <gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
+ <template #startLine>
+ <span :class="getLineClasses(startLineNumber)">{{ startLineNumber }}</span>
+ </template>
+ <template #endLine>
+ <span :class="getLineClasses(endLineNumber)">{{ endLineNumber }}</span>
+ </template>
+ </gl-sprintf>
</div>
<div v-once class="timeline-icon">
<user-avatar-link
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index faa6006945d..fb18be9386e 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 { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
-import Flash from '../../flash';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import * as constants from '../constants';
import eventHub from '../event_hub';
import noteableNote from './noteable_note.vue';
@@ -136,6 +136,8 @@ export default {
}
window.addEventListener('hashchange', this.handleHashChanged);
+
+ eventHub.$on('notesApp.updateIssuableConfidentiality', this.setConfidentiality);
},
updated() {
this.$nextTick(() => {
@@ -146,6 +148,7 @@ export default {
beforeDestroy() {
this.stopPolling();
window.removeEventListener('hashchange', this.handleHashChanged);
+ eventHub.$off('notesApp.updateIssuableConfidentiality', this.setConfidentiality);
},
methods: {
...mapActions([
@@ -164,6 +167,7 @@ export default {
'startTaskList',
'convertToDiscussion',
'stopPolling',
+ 'setConfidentiality',
]),
discussionIsIndividualNoteAndNotConverted(discussion) {
return discussion.individual_note && !this.convertedDisscussionIds.includes(discussion.id);
diff --git a/app/assets/javascripts/notes/event_hub.js b/app/assets/javascripts/notes/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/notes/event_hub.js
+++ b/app/assets/javascripts/notes/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index ba814649078..7bf465482b3 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -20,6 +20,11 @@ document.addEventListener('DOMContentLoaded', () => {
noteableData.noteableType = notesDataset.noteableType;
noteableData.targetType = notesDataset.targetType;
+ if (noteableData.discussion_locked === null) {
+ // discussion_locked has never been set for this issuable.
+ // set to `false` for safety.
+ noteableData.discussion_locked = false;
+ }
if (parsedUserData) {
currentUserData = {
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 9a2e86aeed2..c4a42eb1a98 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 { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
import { clearDraft } from '~/lib/utils/autosave';
import { formatLineRange } from '~/notes/components/multiline_comment_utils';
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index 889883a23d0..61298a15c5d 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -78,7 +78,7 @@ function handleDiscussionJump(self, fn, discussionId = self.currentDiscussionId)
const isDiffView = window.mrTabs.currentAction === 'diffs';
const targetId = fn(discussionId, isDiffView);
const discussion = self.getDiscussion(targetId);
- const discussionFilePath = discussion.diff_file?.file_path;
+ const discussionFilePath = discussion?.diff_file?.file_path;
if (discussionFilePath) {
self.scrollToFile(discussionFilePath);
@@ -113,6 +113,14 @@ export default {
handleDiscussionJump(this, this.previousUnresolvedDiscussionId);
},
+ jumpToFirstUnresolvedDiscussion() {
+ this.setCurrentDiscussionId(null)
+ .then(() => {
+ this.jumpToNextDiscussion();
+ })
+ .catch(() => {});
+ },
+
/**
* Go to the next discussion from the given discussionId
* @param {String} discussionId The id we are jumping from
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index 16b7598ee09..087b5828cce 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -1,4 +1,4 @@
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 5b2ab255557..f6069b509e8 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -3,7 +3,7 @@ import $ from 'jquery';
import Visibility from 'visibilityjs';
import axios from '~/lib/utils/axios_utils';
import TaskList from '../../task_list';
-import Flash from '../../flash';
+import { deprecatedCreateFlash as Flash } from '../../flash';
import Poll from '../../lib/utils/poll';
import * as types from './mutation_types';
import * as utils from './utils';
@@ -13,32 +13,35 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
-import updateIssueConfidentialMutation from '~/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql';
+import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
+import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
+import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
import { __, sprintf } from '~/locale';
import Api from '~/api';
let eTagPoll;
-export const updateConfidentialityOnIssue = ({ commit, getters }, { confidential, fullPath }) => {
- const { iid } = getters.getNoteableData;
+export const updateLockedAttribute = ({ commit, getters }, { locked, fullPath }) => {
+ const { iid, targetType } = getters.getNoteableData;
return utils.gqClient
.mutate({
- mutation: updateIssueConfidentialMutation,
+ mutation: targetType === 'issue' ? updateIssueLockMutation : updateMergeRequestLockMutation,
variables: {
input: {
projectPath: fullPath,
iid: String(iid),
- confidential,
+ locked,
},
},
})
.then(({ data }) => {
- const {
- issueSetConfidential: { issue },
- } = data;
+ const discussionLocked =
+ targetType === 'issue'
+ ? data.issueSetLocked.issue.discussionLocked
+ : data.mergeRequestSetLocked.mergeRequest.discussionLocked;
- commit(types.SET_ISSUE_CONFIDENTIAL, issue.confidential);
+ commit(types.SET_ISSUABLE_LOCK, discussionLocked);
});
};
@@ -683,5 +686,32 @@ export const updateAssignees = ({ commit }, assignees) => {
commit(types.UPDATE_ASSIGNEES, assignees);
};
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
+export const updateDiscussionPosition = ({ commit }, updatedPosition) => {
+ commit(types.UPDATE_DISCUSSION_POSITION, updatedPosition);
+};
+
+export const updateConfidentialityOnIssuable = (
+ { getters, commit },
+ { confidential, fullPath },
+) => {
+ const { iid } = getters.getNoteableData;
+
+ return utils.gqClient
+ .mutate({
+ mutation: updateIssueConfidentialMutation,
+ variables: {
+ input: {
+ projectPath: fullPath,
+ iid: String(iid),
+ confidential,
+ },
+ },
+ })
+ .then(({ data }) => {
+ const {
+ issueSetConfidential: { issue },
+ } = data;
+
+ setConfidentiality({ commit }, issue.confidential);
+ });
+};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 85997b44bcc..7d60fbffb10 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -194,7 +194,9 @@ export const findUnresolvedDiscussionIdNeighbor = (state, getters) => ({
diffOrder,
step,
}) => {
- const ids = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
+ const diffIds = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
+ const dateIds = getters.unresolvedDiscussionsIdsOrdered(false);
+ const ids = diffIds.length ? diffIds : dateIds;
const index = ids.indexOf(discussionId) + step;
if (index < 0 && step < 0) {
@@ -229,6 +231,3 @@ export const getDiscussion = state => discussionId =>
state.discussions.find(discussion => discussion.id === discussionId);
export const commentsDisabled = state => state.commentsDisabled;
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index f2236b18beb..eb3447291bc 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -12,6 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export const TOGGLE_AWARD = 'TOGGLE_AWARD';
export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
+export const UPDATE_DISCUSSION_POSITION = 'UPDATE_DISCUSSION_POSITION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
@@ -42,6 +43,7 @@ export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING';
export const SET_ISSUE_CONFIDENTIAL = 'SET_ISSUE_CONFIDENTIAL';
+export const SET_ISSUABLE_LOCK = 'SET_ISSUABLE_LOCK';
// Description version
export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index e5f1c11fb35..aa078f00569 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -99,6 +99,10 @@ export default {
state.noteableData.confidential = data;
},
+ [types.SET_ISSUABLE_LOCK](state, locked) {
+ state.noteableData.discussion_locked = locked;
+ },
+
[types.SET_USER_DATA](state, data) {
Object.assign(state, { userData: data });
},
@@ -274,6 +278,11 @@ export default {
Object.assign(selectedDiscussion, { ...note });
},
+ [types.UPDATE_DISCUSSION_POSITION](state, { discussionId, position }) {
+ const selectedDiscussion = state.discussions.find(disc => disc.id === discussionId);
+ if (selectedDiscussion) Object.assign(selectedDiscussion.position, { ...position });
+ },
+
[types.CLOSE_ISSUE](state) {
Object.assign(state.noteableData, { state: constants.CLOSED });
},
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index 07e69fa297a..47fb5b271d1 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import Flash from './flash';
+import { deprecatedCreateFlash as Flash } from './flash';
import { __ } from '~/locale';
export default function notificationsDropdown() {
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index fa27994f598..0d1b95f75f8 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
export default class NotificationsForm {
constructor() {
diff --git a/app/assets/javascripts/onboarding_issues/index.js b/app/assets/javascripts/onboarding_issues/index.js
index 5a6f952ffdf..27f2f7f0e9d 100644
--- a/app/assets/javascripts/onboarding_issues/index.js
+++ b/app/assets/javascripts/onboarding_issues/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { parseBoolean, getCookie, setCookie, removeCookie } from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import Tracking from '~/tracking';
const COOKIE_NAME = 'onboarding_issues_settings';
@@ -94,8 +94,12 @@ export const showLearnGitLabProjectPopover = () => {
if (!el) return;
const options = {
- content: __(
- 'Go to <strong>Issues</strong> > <strong>Boards</strong> to access your personalized learning issue board.',
+ content: sprintf(
+ __(
+ 'Go to %{strongStart}Issues%{strongEnd} &gt; %{strongStart}Boards%{strongEnd} to access your personalized learning issue board.',
+ ),
+ { strongStart: '<strong>', strongEnd: '</strong>' },
+ false,
),
};
@@ -111,8 +115,12 @@ export const showLearnGitLabIssuesPopover = () => {
if (!el) return;
const options = {
- content: __(
- 'Go to <strong>Issues</strong> > <strong>Boards</strong> to access your personalized learning issue board.',
+ content: sprintf(
+ __(
+ 'Go to %{strongStart}Issues%{strongEnd} &gt; %{strongStart}Boards%{strongEnd} to access your personalized learning issue board.',
+ ),
+ { strongStart: '<strong>', strongEnd: '</strong>' },
+ false,
),
};
diff --git a/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue b/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue
index 42c9d876595..d83146d2f5e 100644
--- a/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue
+++ b/app/assets/javascripts/operation_settings/components/form_group/dashboard_timezone.vue
@@ -1,7 +1,7 @@
<script>
-import { s__ } from '~/locale';
import { mapState, mapActions } from 'vuex';
import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
+import { s__ } from '~/locale';
import { timezones } from '~/monitoring/format_date';
export default {
diff --git a/app/assets/javascripts/operation_settings/components/metrics_settings.vue b/app/assets/javascripts/operation_settings/components/metrics_settings.vue
index 77c356e5a7f..9df6a412930 100644
--- a/app/assets/javascripts/operation_settings/components/metrics_settings.vue
+++ b/app/assets/javascripts/operation_settings/components/metrics_settings.vue
@@ -1,12 +1,12 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlDeprecatedButton, GlLink } from '@gitlab/ui';
+import { GlButton, GlLink } from '@gitlab/ui';
import ExternalDashboard from './form_group/external_dashboard.vue';
import DashboardTimezone from './form_group/dashboard_timezone.vue';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlLink,
ExternalDashboard,
DashboardTimezone,
@@ -32,9 +32,9 @@ export default {
<section class="settings no-animate">
<div class="settings-header">
<h3 class="js-section-header h4">
- {{ s__('MetricsSettings|Metrics Dashboard') }}
+ {{ s__('MetricsSettings|Metrics dashboard') }}
</h3>
- <gl-deprecated-button class="js-settings-toggle">{{ __('Expand') }}</gl-deprecated-button>
+ <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
{{ s__('MetricsSettings|Manage Metrics Dashboard settings.') }}
<gl-link :href="helpPage">{{ __('Learn more') }}</gl-link>
@@ -44,9 +44,11 @@ export default {
<form>
<dashboard-timezone />
<external-dashboard />
- <gl-deprecated-button variant="success" @click="saveChanges">
- {{ __('Save Changes') }}
- </gl-deprecated-button>
+ <div class="gl-display-flex gl-justify-content-end">
+ <gl-button variant="success" category="primary" @click="saveChanges">
+ {{ __('Save Changes') }}
+ </gl-button>
+ </div>
</form>
</div>
</section>
diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js
index 122acb6bdcf..1d3adeefbd8 100644
--- a/app/assets/javascripts/operation_settings/store/actions.js
+++ b/app/assets/javascripts/operation_settings/store/actions.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import * as mutationTypes from './mutation_types';
@@ -37,6 +37,3 @@ export const receiveSaveChangesError = (_, error) => {
createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/packages/details/components/additional_metadata.vue b/app/assets/javascripts/packages/details/components/additional_metadata.vue
new file mode 100644
index 00000000000..a3de6dd46c7
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/additional_metadata.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import DetailsRow from '~/registry/shared/components/details_row.vue';
+import { generateConanRecipe } from '../utils';
+import { PackageType } from '../../shared/constants';
+
+export default {
+ i18n: {
+ sourceText: s__('PackageRegistry|Source project located at %{link}'),
+ licenseText: s__('PackageRegistry|License information located at %{link}'),
+ recipeText: s__('PackageRegistry|Recipe: %{recipe}'),
+ appGroup: s__('PackageRegistry|App group: %{group}'),
+ appName: s__('PackageRegistry|App name: %{name}'),
+ },
+ components: {
+ DetailsRow,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ conanRecipe() {
+ return generateConanRecipe(this.packageEntity);
+ },
+ showMetadata() {
+ const visibilityConditions = {
+ [PackageType.NUGET]: this.packageEntity.nuget_metadatum,
+ [PackageType.CONAN]: this.packageEntity.conan_metadatum,
+ [PackageType.MAVEN]: this.packageEntity.maven_metadatum,
+ };
+ return visibilityConditions[this.packageEntity.package_type];
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="showMetadata">
+ <h3 class="gl-font-lg" data-testid="title">{{ __('Additional Metadata') }}</h3>
+
+ <div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main">
+ <template v-if="packageEntity.nuget_metadatum">
+ <details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
+ <gl-sprintf :message="$options.i18n.sourceText">
+ <template #link>
+ <gl-link :href="packageEntity.nuget_metadatum.project_url" target="_blank">{{
+ packageEntity.nuget_metadatum.project_url
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ <details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
+ <gl-sprintf :message="$options.i18n.licenseText">
+ <template #link>
+ <gl-link :href="packageEntity.nuget_metadatum.license_url" target="_blank">{{
+ packageEntity.nuget_metadatum.license_url
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ </template>
+
+ <details-row
+ v-else-if="packageEntity.conan_metadatum"
+ icon="information-o"
+ padding="gl-p-4"
+ data-testid="conan-recipe"
+ >
+ <gl-sprintf :message="$options.i18n.recipeText">
+ <template #recipe>{{ conanRecipe }}</template>
+ </gl-sprintf>
+ </details-row>
+
+ <template v-else-if="packageEntity.maven_metadatum">
+ <details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app">
+ <gl-sprintf :message="$options.i18n.appName">
+ <template #name>
+ <strong>{{ packageEntity.maven_metadatum.app_name }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ <details-row icon="information-o" padding="gl-p-4" data-testid="maven-group">
+ <gl-sprintf :message="$options.i18n.appGroup">
+ <template #group>
+ <strong>{{ packageEntity.maven_metadatum.app_group }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue
new file mode 100644
index 00000000000..dbb5f7be0a0
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/app.vue
@@ -0,0 +1,289 @@
+<script>
+import {
+ GlBadge,
+ GlButton,
+ GlModal,
+ GlModalDirective,
+ GlTooltipDirective,
+ GlLink,
+ GlEmptyState,
+ GlTab,
+ GlTabs,
+ GlTable,
+ GlSprintf,
+} from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import Tracking from '~/tracking';
+import PackageHistory from './package_history.vue';
+import PackageTitle from './package_title.vue';
+import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
+import PackageListRow from '../../shared/components/package_list_row.vue';
+import DependencyRow from './dependency_row.vue';
+import AdditionalMetadata from './additional_metadata.vue';
+import InstallationCommands from './installation_commands.vue';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import { __, s__ } from '~/locale';
+import { PackageType, TrackingActions } from '../../shared/constants';
+import { packageTypeToTrackCategory } from '../../shared/utils';
+
+export default {
+ name: 'PackagesApp',
+ components: {
+ GlBadge,
+ GlButton,
+ GlEmptyState,
+ GlLink,
+ GlModal,
+ GlTab,
+ GlTabs,
+ GlTable,
+ FileIcon,
+ GlSprintf,
+ PackageTitle,
+ PackagesListLoader,
+ PackageListRow,
+ DependencyRow,
+ PackageHistory,
+ AdditionalMetadata,
+ InstallationCommands,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlModal: GlModalDirective,
+ },
+ mixins: [timeagoMixin, Tracking.mixin()],
+ trackingActions: { ...TrackingActions },
+ computed: {
+ ...mapState([
+ 'projectName',
+ 'packageEntity',
+ 'packageFiles',
+ 'isLoading',
+ 'canDelete',
+ 'destroyPath',
+ 'svgPath',
+ 'npmPath',
+ 'npmHelpPath',
+ ]),
+ isValidPackage() {
+ return Boolean(this.packageEntity.name);
+ },
+ canDeletePackage() {
+ return this.canDelete && this.destroyPath;
+ },
+ filesTableRows() {
+ return this.packageFiles.map(x => ({
+ name: x.file_name,
+ downloadPath: x.download_path,
+ size: this.formatSize(x.size),
+ created: x.created_at,
+ }));
+ },
+ tracking() {
+ return {
+ category: packageTypeToTrackCategory(this.packageEntity.package_type),
+ };
+ },
+ hasVersions() {
+ return this.packageEntity.versions?.length > 0;
+ },
+ packageDependencies() {
+ return this.packageEntity.dependency_links || [];
+ },
+ showDependencies() {
+ return this.packageEntity.package_type === PackageType.NUGET;
+ },
+ showFiles() {
+ return this.packageEntity?.package_type !== PackageType.COMPOSER;
+ },
+ },
+ methods: {
+ ...mapActions(['fetchPackageVersions']),
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+ cancelDelete() {
+ this.$refs.deleteModal.hide();
+ },
+ getPackageVersions() {
+ if (!this.packageEntity.versions) {
+ this.fetchPackageVersions();
+ }
+ },
+ },
+ i18n: {
+ deleteModalTitle: s__(`PackageRegistry|Delete Package Version`),
+ deleteModalContent: s__(
+ `PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`,
+ ),
+ },
+ filesTableHeaderFields: [
+ {
+ key: 'name',
+ label: __('Name'),
+ tdClass: 'd-flex align-items-center',
+ },
+ {
+ key: 'size',
+ label: __('Size'),
+ },
+ {
+ key: 'created',
+ label: __('Created'),
+ class: 'text-right',
+ },
+ ],
+};
+</script>
+
+<template>
+ <gl-empty-state
+ v-if="!isValidPackage"
+ :title="s__('PackageRegistry|Unable to load package')"
+ :description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
+ :svg-path="svgPath"
+ />
+
+ <div v-else class="packages-app">
+ <div class="detail-page-header d-flex justify-content-between flex-column flex-sm-row">
+ <package-title />
+
+ <div class="mt-sm-2">
+ <gl-button
+ v-if="canDeletePackage"
+ v-gl-modal="'delete-modal'"
+ class="js-delete-button"
+ variant="danger"
+ category="primary"
+ data-qa-selector="delete_button"
+ >
+ {{ __('Delete') }}
+ </gl-button>
+ </div>
+ </div>
+
+ <gl-tabs>
+ <gl-tab :title="__('Detail')">
+ <div data-qa-selector="package_information_content">
+ <package-history :package-entity="packageEntity" :project-name="projectName" />
+
+ <installation-commands
+ :package-entity="packageEntity"
+ :npm-path="npmPath"
+ :npm-help-path="npmHelpPath"
+ />
+
+ <additional-metadata :package-entity="packageEntity" />
+ </div>
+
+ <template v-if="showFiles">
+ <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3>
+ <gl-table
+ :fields="$options.filesTableHeaderFields"
+ :items="filesTableRows"
+ tbody-tr-class="js-file-row"
+ >
+ <template #cell(name)="{ item }">
+ <gl-link
+ :href="item.downloadPath"
+ class="js-file-download gl-relative"
+ @click="track($options.trackingActions.PULL_PACKAGE)"
+ >
+ <file-icon
+ :file-name="item.name"
+ css-classes="gl-relative file-icon"
+ class="gl-mr-1 gl-relative"
+ />
+ <span class="gl-relative">{{ item.name }}</span>
+ </gl-link>
+ </template>
+
+ <template #cell(created)="{ item }">
+ <span v-gl-tooltip :title="tooltipTitle(item.created)">{{
+ timeFormatted(item.created)
+ }}</span>
+ </template>
+ </gl-table>
+ </template>
+ </gl-tab>
+
+ <gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab">
+ <template #title>
+ <span>{{ __('Dependencies') }}</span>
+ <gl-badge size="sm" data-testid="dependencies-badge">{{
+ packageDependencies.length
+ }}</gl-badge>
+ </template>
+
+ <template v-if="packageDependencies.length > 0">
+ <dependency-row
+ v-for="(dep, index) in packageDependencies"
+ :key="index"
+ :dependency="dep"
+ />
+ </template>
+
+ <p v-else class="gl-mt-3" data-testid="no-dependencies-message">
+ {{ s__('PackageRegistry|This NuGet package has no dependencies.') }}
+ </p>
+ </gl-tab>
+
+ <gl-tab
+ :title="__('Other versions')"
+ title-item-class="js-versions-tab"
+ @click="getPackageVersions"
+ >
+ <template v-if="isLoading && !hasVersions">
+ <packages-list-loader />
+ </template>
+
+ <template v-else-if="hasVersions">
+ <package-list-row
+ v-for="v in packageEntity.versions"
+ :key="v.id"
+ :package-entity="{ name: packageEntity.name, ...v }"
+ :package-link="v.id.toString()"
+ :disable-delete="true"
+ :show-package-type="false"
+ />
+ </template>
+
+ <p v-else class="gl-mt-3" data-testid="no-versions-message">
+ {{ s__('PackageRegistry|There are no other versions of this package.') }}
+ </p>
+ </gl-tab>
+ </gl-tabs>
+
+ <gl-modal ref="deleteModal" class="js-delete-modal" modal-id="delete-modal">
+ <template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
+ <gl-sprintf :message="$options.i18n.deleteModalContent">
+ <template #version>
+ <strong>{{ packageEntity.version }}</strong>
+ </template>
+
+ <template #name>
+ <strong>{{ packageEntity.name }}</strong>
+ </template>
+ </gl-sprintf>
+
+ <div slot="modal-footer" class="w-100">
+ <div class="float-right">
+ <gl-button @click="cancelDelete()">{{ __('Cancel') }}</gl-button>
+ <gl-button
+ ref="modal-delete-button"
+ data-method="delete"
+ :to="destroyPath"
+ variant="danger"
+ category="primary"
+ data-qa-selector="delete_modal_button"
+ @click="track($options.trackingActions.DELETE_PACKAGE)"
+ >
+ {{ __('Delete') }}
+ </gl-button>
+ </div>
+ </div>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/code_instruction.vue b/app/assets/javascripts/packages/details/components/code_instruction.vue
new file mode 100644
index 00000000000..0719ddfcd2b
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/code_instruction.vue
@@ -0,0 +1,63 @@
+<script>
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Tracking from '~/tracking';
+import { TrackingLabels } from '../constants';
+
+export default {
+ name: 'CodeInstruction',
+ components: {
+ ClipboardButton,
+ },
+ mixins: [
+ Tracking.mixin({
+ label: TrackingLabels.CODE_INSTRUCTION,
+ }),
+ ],
+ props: {
+ instruction: {
+ type: String,
+ required: true,
+ },
+ copyText: {
+ type: String,
+ required: true,
+ },
+ multiline: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ trackingAction: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ methods: {
+ trackCopy() {
+ if (this.trackingAction) {
+ this.track(this.trackingAction);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="!multiline" class="input-group gl-mb-3">
+ <input
+ :value="instruction"
+ type="text"
+ class="form-control monospace js-instruction-input"
+ readonly
+ @copy="trackCopy"
+ />
+ <span class="input-group-append js-instruction-button" @click="trackCopy">
+ <clipboard-button :text="instruction" :title="copyText" class="input-group-text" />
+ </span>
+ </div>
+
+ <div v-else>
+ <pre class="js-instruction-pre" @copy="trackCopy">{{ instruction }}</pre>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/composer_installation.vue b/app/assets/javascripts/packages/details/components/composer_installation.vue
new file mode 100644
index 00000000000..1934da149ce
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/composer_installation.vue
@@ -0,0 +1,60 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { mapGetters, mapState } from 'vuex';
+import { s__ } from '~/locale';
+import CodeInstruction from './code_instruction.vue';
+import { TrackingActions } from '../constants';
+
+export default {
+ name: 'ComposerInstallation',
+ components: {
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ computed: {
+ ...mapState(['composerHelpPath']),
+ ...mapGetters(['composerRegistryInclude', 'composerPackageInclude']),
+ },
+ i18n: {
+ registryInclude: s__('PackageRegistry|composer.json registry include'),
+ copyRegistryInclude: s__('PackageRegistry|Copy registry include'),
+ packageInclude: s__('PackageRegistry|composer.json require package include'),
+ copyPackageInclude: s__('PackageRegistry|Copy require package include'),
+ infoLine: s__(
+ 'PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}',
+ ),
+ },
+ trackingActions: { ...TrackingActions },
+};
+</script>
+
+<template>
+ <div>
+ <h3 class="gl-font-lg">{{ __('Installation') }}</h3>
+ <h4 class="gl-font-base" data-testid="registry-include-title">
+ {{ $options.i18n.registryInclude }}
+ </h4>
+
+ <code-instruction
+ :instruction="composerRegistryInclude"
+ :copy-text="$options.i18n.copyRegistryInclude"
+ :tracking-action="$options.trackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND"
+ />
+ <h4 class="gl-font-base" data-testid="package-include-title">
+ {{ $options.i18n.packageInclude }}
+ </h4>
+ <code-instruction
+ :instruction="composerPackageInclude"
+ :copy-text="$options.i18n.copyPackageInclude"
+ :tracking-action="$options.trackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND"
+ />
+ <span data-testid="help-text">
+ <gl-sprintf :message="$options.i18n.infoLine">
+ <template #link="{ content }">
+ <gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/conan_installation.vue b/app/assets/javascripts/packages/details/components/conan_installation.vue
new file mode 100644
index 00000000000..cff7d73f1e8
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/conan_installation.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { mapGetters, mapState } from 'vuex';
+import { s__ } from '~/locale';
+import CodeInstruction from './code_instruction.vue';
+import { TrackingActions } from '../constants';
+
+export default {
+ name: 'ConanInstallation',
+ components: {
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ computed: {
+ ...mapState(['conanHelpPath']),
+ ...mapGetters(['conanInstallationCommand', 'conanSetupCommand']),
+ },
+ i18n: {
+ helpText: s__(
+ 'PackageRegistry|For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}.',
+ ),
+ },
+ trackingActions: { ...TrackingActions },
+};
+</script>
+
+<template>
+ <div>
+ <h3 class="gl-font-lg">{{ __('Installation') }}</h3>
+ <h4 class="gl-font-base">
+ {{ s__('PackageRegistry|Conan Command') }}
+ </h4>
+
+ <code-instruction
+ :instruction="conanInstallationCommand"
+ :copy-text="s__('PackageRegistry|Copy Conan Command')"
+ :tracking-action="$options.trackingActions.COPY_CONAN_COMMAND"
+ />
+
+ <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
+ <h4 class="gl-font-base">
+ {{ s__('PackageRegistry|Add Conan Remote') }}
+ </h4>
+ <code-instruction
+ :instruction="conanSetupCommand"
+ :copy-text="s__('PackageRegistry|Copy Conan Setup Command')"
+ :tracking-action="$options.trackingActions.COPY_CONAN_SETUP_COMMAND"
+ />
+ <gl-sprintf :message="$options.i18n.helpText">
+ <template #link="{ content }">
+ <gl-link :href="conanHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/dependency_row.vue b/app/assets/javascripts/packages/details/components/dependency_row.vue
new file mode 100644
index 00000000000..788673d2881
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/dependency_row.vue
@@ -0,0 +1,35 @@
+<script>
+export default {
+ name: 'DependencyRow',
+ props: {
+ dependency: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ showVersion() {
+ return Boolean(this.dependency.version_pattern);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-responsive-table-row">
+ <div class="table-section section-50">
+ <strong class="gl-text-body">{{ dependency.name }}</strong>
+ <span v-if="dependency.target_framework" data-testid="target-framework"
+ >({{ dependency.target_framework }})</span
+ >
+ </div>
+
+ <div
+ v-if="showVersion"
+ class="table-section section-50 gl-display-flex justify-content-md-end"
+ data-testid="version-pattern"
+ >
+ <span class="gl-text-body">{{ dependency.version_pattern }}</span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/history_element.vue b/app/assets/javascripts/packages/details/components/history_element.vue
new file mode 100644
index 00000000000..8a51c1528cf
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/history_element.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+export default {
+ name: 'HistoryElement',
+ components: {
+ GlIcon,
+ TimelineEntryItem,
+ },
+
+ props: {
+ icon: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <timeline-entry-item class="system-note note-wrapper gl-mb-6!">
+ <div class="timeline-icon">
+ <gl-icon :name="icon" />
+ </div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <span>
+ <slot></slot>
+ </span>
+ </div>
+ <div class="note-body"></div>
+ </div>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/installation_commands.vue b/app/assets/javascripts/packages/details/components/installation_commands.vue
new file mode 100644
index 00000000000..138103020a7
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/installation_commands.vue
@@ -0,0 +1,53 @@
+<script>
+import ConanInstallation from './conan_installation.vue';
+import MavenInstallation from './maven_installation.vue';
+import NpmInstallation from './npm_installation.vue';
+import NugetInstallation from './nuget_installation.vue';
+import PypiInstallation from './pypi_installation.vue';
+import ComposerInstallation from './composer_installation.vue';
+import { PackageType } from '../../shared/constants';
+
+export default {
+ name: 'InstallationCommands',
+ components: {
+ [PackageType.CONAN]: ConanInstallation,
+ [PackageType.MAVEN]: MavenInstallation,
+ [PackageType.NPM]: NpmInstallation,
+ [PackageType.NUGET]: NugetInstallation,
+ [PackageType.PYPI]: PypiInstallation,
+ [PackageType.COMPOSER]: ComposerInstallation,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ npmPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ npmHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ installationComponent() {
+ return this.$options.components[this.packageEntity.package_type];
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="installationComponent">
+ <component
+ :is="installationComponent"
+ :name="packageEntity.name"
+ :registry-url="npmPath"
+ :help-url="npmHelpPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/maven_installation.vue b/app/assets/javascripts/packages/details/components/maven_installation.vue
new file mode 100644
index 00000000000..d6641c886a0
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/maven_installation.vue
@@ -0,0 +1,84 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { mapGetters, mapState } from 'vuex';
+import { s__ } from '~/locale';
+import CodeInstruction from './code_instruction.vue';
+import { TrackingActions } from '../constants';
+
+export default {
+ name: 'MavenInstallation',
+ components: {
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ computed: {
+ ...mapState(['mavenHelpPath']),
+ ...mapGetters(['mavenInstallationXml', 'mavenInstallationCommand', 'mavenSetupXml']),
+ },
+ i18n: {
+ xmlText: s__(
+ `PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block.`,
+ ),
+ setupText: s__(
+ `PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file.`,
+ ),
+ helpText: s__(
+ 'PackageRegistry|For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}.',
+ ),
+ },
+ trackingActions: { ...TrackingActions },
+};
+</script>
+
+<template>
+ <div>
+ <h3 class="gl-font-lg">{{ __('Installation') }}</h3>
+
+ <h4 class="gl-font-base">
+ {{ s__('PackageRegistry|Maven XML') }}
+ </h4>
+ <p>
+ <gl-sprintf :message="$options.i18n.xmlText">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <code-instruction
+ :instruction="mavenInstallationXml"
+ :copy-text="s__('PackageRegistry|Copy Maven XML')"
+ multiline
+ :tracking-action="$options.trackingActions.COPY_MAVEN_XML"
+ />
+
+ <h4 class="gl-font-base">
+ {{ s__('PackageRegistry|Maven Command') }}
+ </h4>
+ <code-instruction
+ :instruction="mavenInstallationCommand"
+ :copy-text="s__('PackageRegistry|Copy Maven command')"
+ :tracking-action="$options.trackingActions.COPY_MAVEN_COMMAND"
+ />
+
+ <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
+ <p>
+ <gl-sprintf :message="$options.i18n.setupText">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <code-instruction
+ :instruction="mavenSetupXml"
+ :copy-text="s__('PackageRegistry|Copy Maven registry XML')"
+ multiline
+ :tracking-action="$options.trackingActions.COPY_MAVEN_SETUP"
+ />
+ <gl-sprintf :message="$options.i18n.helpText">
+ <template #link="{ content }">
+ <gl-link :href="mavenHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/npm_installation.vue b/app/assets/javascripts/packages/details/components/npm_installation.vue
new file mode 100644
index 00000000000..d7ff8428370
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/npm_installation.vue
@@ -0,0 +1,80 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { mapGetters, mapState } from 'vuex';
+import { s__ } from '~/locale';
+import CodeInstruction from './code_instruction.vue';
+import { NpmManager, TrackingActions } from '../constants';
+
+export default {
+ name: 'NpmInstallation',
+ components: {
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ computed: {
+ ...mapState(['npmHelpPath']),
+ ...mapGetters(['npmInstallationCommand', 'npmSetupCommand']),
+ npmCommand() {
+ return this.npmInstallationCommand(NpmManager.NPM);
+ },
+ npmSetup() {
+ return this.npmSetupCommand(NpmManager.NPM);
+ },
+ yarnCommand() {
+ return this.npmInstallationCommand(NpmManager.YARN);
+ },
+ yarnSetupCommand() {
+ return this.npmSetupCommand(NpmManager.YARN);
+ },
+ },
+ i18n: {
+ helpText: s__(
+ 'PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more.',
+ ),
+ },
+ trackingActions: { ...TrackingActions },
+};
+</script>
+
+<template>
+ <div>
+ <h3 class="gl-font-lg">{{ __('Installation') }}</h3>
+ <h4 class="gl-font-base">{{ s__('PackageRegistry|npm command') }}</h4>
+
+ <code-instruction
+ :instruction="npmCommand"
+ :copy-text="s__('PackageRegistry|Copy npm command')"
+ :tracking-action="$options.trackingActions.COPY_NPM_INSTALL_COMMAND"
+ />
+
+ <h4 class="gl-font-base">{{ s__('PackageRegistry|yarn command') }}</h4>
+ <code-instruction
+ :instruction="yarnCommand"
+ :copy-text="s__('PackageRegistry|Copy yarn command')"
+ :tracking-action="$options.trackingActions.COPY_YARN_INSTALL_COMMAND"
+ />
+
+ <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
+
+ <h4 class="gl-font-base">{{ s__('PackageRegistry|npm command') }}</h4>
+ <code-instruction
+ :instruction="npmSetup"
+ :copy-text="s__('PackageRegistry|Copy npm setup command')"
+ :tracking-action="$options.trackingActions.COPY_NPM_SETUP_COMMAND"
+ />
+
+ <h4 class="gl-font-base">{{ s__('PackageRegistry|yarn command') }}</h4>
+ <code-instruction
+ :instruction="yarnSetupCommand"
+ :copy-text="s__('PackageRegistry|Copy yarn setup command')"
+ :tracking-action="$options.trackingActions.COPY_YARN_SETUP_COMMAND"
+ />
+
+ <gl-sprintf :message="$options.i18n.helpText">
+ <template #link="{ content }">
+ <gl-link :href="npmHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/nuget_installation.vue b/app/assets/javascripts/packages/details/components/nuget_installation.vue
new file mode 100644
index 00000000000..150b6e3ab0f
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/nuget_installation.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { mapGetters, mapState } from 'vuex';
+import { s__ } from '~/locale';
+import CodeInstruction from './code_instruction.vue';
+import { TrackingActions } from '../constants';
+
+export default {
+ name: 'NugetInstallation',
+ components: {
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ computed: {
+ ...mapState(['nugetHelpPath']),
+ ...mapGetters(['nugetInstallationCommand', 'nugetSetupCommand']),
+ },
+ i18n: {
+ helpText: s__(
+ 'PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}.',
+ ),
+ },
+ trackingActions: { ...TrackingActions },
+};
+</script>
+
+<template>
+ <div>
+ <h3 class="gl-font-lg">{{ __('Installation') }}</h3>
+ <h4 class="gl-font-base">
+ {{ s__('PackageRegistry|NuGet Command') }}
+ </h4>
+ <code-instruction
+ :instruction="nugetInstallationCommand"
+ :copy-text="s__('PackageRegistry|Copy NuGet Command')"
+ :tracking-action="$options.trackingActions.COPY_NUGET_INSTALL_COMMAND"
+ />
+ <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
+ <h4 class="gl-font-base">
+ {{ s__('PackageRegistry|Add NuGet Source') }}
+ </h4>
+
+ <code-instruction
+ :instruction="nugetSetupCommand"
+ :copy-text="s__('PackageRegistry|Copy NuGet Setup Command')"
+ :tracking-action="$options.trackingActions.COPY_NUGET_SETUP_COMMAND"
+ />
+ <gl-sprintf :message="$options.i18n.helpText">
+ <template #link="{ content }">
+ <gl-link :href="nugetHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/package_history.vue b/app/assets/javascripts/packages/details/components/package_history.vue
new file mode 100644
index 00000000000..96ce106884d
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/package_history.vue
@@ -0,0 +1,114 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import HistoryElement from './history_element.vue';
+
+export default {
+ name: 'PackageHistory',
+ i18n: {
+ createdOn: s__('PackageRegistry|%{name} version %{version} was created %{datetime}'),
+ updatedAtText: s__('PackageRegistry|%{name} version %{version} was updated %{datetime}'),
+ commitText: s__('PackageRegistry|Commit %{link} on branch %{branch}'),
+ pipelineText: s__('PackageRegistry|Pipeline %{link} triggered %{datetime} by %{author}'),
+ publishText: s__('PackageRegistry|Published to the %{project} Package Registry %{datetime}'),
+ },
+ components: {
+ GlLink,
+ GlSprintf,
+ HistoryElement,
+ TimeAgoTooltip,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ projectName: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showDescription: false,
+ };
+ },
+ computed: {
+ packagePipeline() {
+ return this.packageEntity.pipeline?.id ? this.packageEntity.pipeline : null;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-discussion">
+ <h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3>
+ <ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
+ <history-element icon="clock" data-testid="created-on">
+ <gl-sprintf :message="$options.i18n.createdOn">
+ <template #name>
+ <strong>{{ packageEntity.name }}</strong>
+ </template>
+ <template #version>
+ <strong>{{ packageEntity.version }}</strong>
+ </template>
+ <template #datetime>
+ <time-ago-tooltip :time="packageEntity.created_at" />
+ </template>
+ </gl-sprintf>
+ </history-element>
+ <history-element icon="pencil" data-testid="updated-at">
+ <gl-sprintf :message="$options.i18n.updatedAtText">
+ <template #name>
+ <strong>{{ packageEntity.name }}</strong>
+ </template>
+ <template #version>
+ <strong>{{ packageEntity.version }}</strong>
+ </template>
+ <template #datetime>
+ <time-ago-tooltip :time="packageEntity.updated_at" />
+ </template>
+ </gl-sprintf>
+ </history-element>
+ <template v-if="packagePipeline">
+ <history-element icon="commit" data-testid="commit">
+ <gl-sprintf :message="$options.i18n.commitText">
+ <template #link>
+ <gl-link :href="packagePipeline.project.commit_url">{{
+ packagePipeline.sha
+ }}</gl-link>
+ </template>
+ <template #branch>
+ <strong>{{ packagePipeline.ref }}</strong>
+ </template>
+ </gl-sprintf>
+ </history-element>
+ <history-element icon="pipeline" data-testid="pipeline">
+ <gl-sprintf :message="$options.i18n.pipelineText">
+ <template #link>
+ <gl-link :href="packagePipeline.project.pipeline_url"
+ >#{{ packagePipeline.id }}</gl-link
+ >
+ </template>
+ <template #datetime>
+ <time-ago-tooltip :time="packagePipeline.created_at" />
+ </template>
+ <template #author>{{ packagePipeline.user.name }}</template>
+ </gl-sprintf>
+ </history-element>
+ </template>
+ <history-element icon="package" data-testid="published">
+ <gl-sprintf :message="$options.i18n.publishText">
+ <template #project>
+ <strong>{{ projectName }}</strong>
+ </template>
+ <template #datetime>
+ <time-ago-tooltip :time="packageEntity.created_at" />
+ </template>
+ </gl-sprintf>
+ </history-element>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/package_title.vue b/app/assets/javascripts/packages/details/components/package_title.vue
new file mode 100644
index 00000000000..d07883e3e7a
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/package_title.vue
@@ -0,0 +1,112 @@
+<script>
+import { mapState, mapGetters } from 'vuex';
+import { GlAvatar, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import PackageTags from '../../shared/components/package_tags.vue';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { __ } from '~/locale';
+
+export default {
+ name: 'PackageTitle',
+ components: {
+ GlAvatar,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ PackageTags,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeagoMixin],
+ computed: {
+ ...mapState(['packageEntity', 'packageFiles']),
+ ...mapGetters(['packageTypeDisplay', 'packagePipeline', 'packageIcon']),
+ hasTagsToDisplay() {
+ return Boolean(this.packageEntity.tags && this.packageEntity.tags.length);
+ },
+ totalSize() {
+ return numberToHumanSize(this.packageFiles.reduce((acc, p) => acc + p.size, 0));
+ },
+ },
+ i18n: {
+ packageInfo: __('v%{version} published %{timeAgo}'),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-flex-direction-column">
+ <div class="gl-display-flex">
+ <gl-avatar
+ v-if="packageIcon"
+ :src="packageIcon"
+ shape="rect"
+ class="gl-align-self-center gl-mr-4"
+ data-testid="package-icon"
+ />
+
+ <div class="gl-display-flex gl-flex-direction-column">
+ <h1 class="gl-font-size-h1 gl-mt-3 gl-mb-2">
+ {{ packageEntity.name }}
+ </h1>
+
+ <div class="gl-display-flex gl-align-items-center gl-text-gray-500">
+ <gl-icon name="eye" class="gl-mr-3" />
+ <gl-sprintf :message="$options.i18n.packageInfo">
+ <template #version>
+ {{ packageEntity.version }}
+ </template>
+
+ <template #timeAgo>
+ <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
+ &nbsp;{{ timeFormatted(packageEntity.created_at) }}
+ </span>
+ </template>
+ </gl-sprintf>
+ </div>
+ </div>
+ </div>
+
+ <div class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3">
+ <div v-if="packageTypeDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
+ <gl-icon name="package" class="gl-text-gray-500 gl-mr-3" />
+ <span data-testid="package-type" class="gl-font-weight-bold">{{ packageTypeDisplay }}</span>
+ </div>
+
+ <div v-if="hasTagsToDisplay" class="gl-display-flex gl-align-items-center gl-mr-5">
+ <package-tags :tag-display-limit="1" :tags="packageEntity.tags" />
+ </div>
+
+ <div v-if="packagePipeline" class="gl-display-flex gl-align-items-center gl-mr-5">
+ <gl-icon name="review-list" class="gl-text-gray-500 gl-mr-3" />
+ <gl-link
+ data-testid="pipeline-project"
+ :href="packagePipeline.project.web_url"
+ class="gl-font-weight-bold text-truncate"
+ >
+ {{ packagePipeline.project.name }}
+ </gl-link>
+ </div>
+
+ <div
+ v-if="packagePipeline"
+ data-testid="package-ref"
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <gl-icon name="branch" class="gl-text-gray-500 gl-mr-3" />
+ <span
+ v-gl-tooltip
+ class="gl-font-weight-bold text-truncate mw-xs"
+ :title="packagePipeline.ref"
+ >{{ packagePipeline.ref }}</span
+ >
+ </div>
+
+ <div class="gl-display-flex gl-align-items-center gl-mr-5">
+ <gl-icon name="disk" class="gl-text-gray-500 gl-mr-3" />
+ <span data-testid="package-size" class="gl-font-weight-bold">{{ totalSize }}</span>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/components/pypi_installation.vue b/app/assets/javascripts/packages/details/components/pypi_installation.vue
new file mode 100644
index 00000000000..f1c619fd6d3
--- /dev/null
+++ b/app/assets/javascripts/packages/details/components/pypi_installation.vue
@@ -0,0 +1,68 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { mapGetters, mapState } from 'vuex';
+import { s__ } from '~/locale';
+import CodeInstruction from './code_instruction.vue';
+import { TrackingActions } from '../constants';
+
+export default {
+ name: 'PyPiInstallation',
+ components: {
+ CodeInstruction,
+ GlLink,
+ GlSprintf,
+ },
+ computed: {
+ ...mapState(['pypiHelpPath']),
+ ...mapGetters(['pypiPipCommand', 'pypiSetupCommand']),
+ },
+ i18n: {
+ setupText: s__(
+ `PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file.`,
+ ),
+ helpText: s__(
+ 'PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}.',
+ ),
+ },
+ trackingActions: { ...TrackingActions },
+};
+</script>
+
+<template>
+ <div>
+ <h3 class="gl-font-lg">{{ __('Installation') }}</h3>
+
+ <h4 class="gl-font-base">
+ {{ s__('PackageRegistry|Pip Command') }}
+ </h4>
+
+ <code-instruction
+ :instruction="pypiPipCommand"
+ :copy-text="s__('PackageRegistry|Copy Pip command')"
+ data-testid="pip-command"
+ :tracking-action="$options.trackingActions.COPY_PIP_INSTALL_COMMAND"
+ />
+
+ <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3>
+ <p>
+ <gl-sprintf :message="$options.i18n.setupText">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <code-instruction
+ :instruction="pypiSetupCommand"
+ :copy-text="s__('PackageRegistry|Copy .pypirc content')"
+ data-testid="pypi-setup-content"
+ multiline
+ :tracking-action="$options.trackingActions.COPY_PYPI_SETUP_COMMAND"
+ />
+ <gl-sprintf :message="$options.i18n.helpText">
+ <template #link="{ content }">
+ <gl-link :href="pypiHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/details/constants.js b/app/assets/javascripts/packages/details/constants.js
new file mode 100644
index 00000000000..c6e1b388132
--- /dev/null
+++ b/app/assets/javascripts/packages/details/constants.js
@@ -0,0 +1,47 @@
+import { s__ } from '~/locale';
+
+export const TrackingLabels = {
+ CODE_INSTRUCTION: 'code_instruction',
+ CONAN_INSTALLATION: 'conan_installation',
+ MAVEN_INSTALLATION: 'maven_installation',
+ NPM_INSTALLATION: 'npm_installation',
+ NUGET_INSTALLATION: 'nuget_installation',
+ PYPI_INSTALLATION: 'pypi_installation',
+ COMPOSER_INSTALLATION: 'composer_installation',
+};
+
+export const TrackingActions = {
+ INSTALLATION: 'installation',
+ REGISTRY_SETUP: 'registry_setup',
+
+ COPY_CONAN_COMMAND: 'copy_conan_command',
+ COPY_CONAN_SETUP_COMMAND: 'copy_conan_setup_command',
+
+ COPY_MAVEN_XML: 'copy_maven_xml',
+ COPY_MAVEN_COMMAND: 'copy_maven_command',
+ COPY_MAVEN_SETUP: 'copy_maven_setup_xml',
+
+ COPY_NPM_INSTALL_COMMAND: 'copy_npm_install_command',
+ COPY_NPM_SETUP_COMMAND: 'copy_npm_setup_command',
+
+ COPY_YARN_INSTALL_COMMAND: 'copy_yarn_install_command',
+ COPY_YARN_SETUP_COMMAND: 'copy_yarn_setup_command',
+
+ COPY_NUGET_INSTALL_COMMAND: 'copy_nuget_install_command',
+ COPY_NUGET_SETUP_COMMAND: 'copy_nuget_setup_command',
+
+ COPY_PIP_INSTALL_COMMAND: 'copy_pip_install_command',
+ COPY_PYPI_SETUP_COMMAND: 'copy_pypi_setup_command',
+
+ COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND: 'copy_composer_registry_include_command',
+ COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND: 'copy_composer_package_include_command',
+};
+
+export const NpmManager = {
+ NPM: 'npm',
+ YARN: 'yarn',
+};
+
+export const FETCH_PACKAGE_VERSIONS_ERROR = s__(
+ 'PackageRegistry|Unable to fetch package version information.',
+);
diff --git a/app/assets/javascripts/packages/details/index.js b/app/assets/javascripts/packages/details/index.js
new file mode 100644
index 00000000000..233da3e4a99
--- /dev/null
+++ b/app/assets/javascripts/packages/details/index.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import PackagesApp from './components/app.vue';
+import Translate from '~/vue_shared/translate';
+import createStore from './store';
+
+Vue.use(Translate);
+
+export default () => {
+ const el = document.querySelector('#js-vue-packages-detail');
+ const { package: packageJson, canDelete: canDeleteStr, ...rest } = el.dataset;
+ const packageEntity = JSON.parse(packageJson);
+ const canDelete = canDeleteStr === 'true';
+
+ const store = createStore({
+ packageEntity,
+ packageFiles: packageEntity.package_files,
+ canDelete,
+ ...rest,
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ PackagesApp,
+ },
+ store,
+ render(createElement) {
+ return createElement('packages-app');
+ },
+ });
+};
diff --git a/app/assets/javascripts/packages/details/store/actions.js b/app/assets/javascripts/packages/details/store/actions.js
new file mode 100644
index 00000000000..cda80056e19
--- /dev/null
+++ b/app/assets/javascripts/packages/details/store/actions.js
@@ -0,0 +1,23 @@
+import Api from '~/api';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants';
+import * as types from './mutation_types';
+
+export default ({ commit, state }) => {
+ commit(types.SET_LOADING, true);
+
+ const { project_id, id } = state.packageEntity;
+
+ return Api.projectPackage(project_id, id)
+ .then(({ data }) => {
+ if (data.versions) {
+ commit(types.SET_PACKAGE_VERSIONS, data.versions.reverse());
+ }
+ })
+ .catch(() => {
+ createFlash(FETCH_PACKAGE_VERSIONS_ERROR);
+ })
+ .finally(() => {
+ commit(types.SET_LOADING, false);
+ });
+};
diff --git a/app/assets/javascripts/packages/details/store/getters.js b/app/assets/javascripts/packages/details/store/getters.js
new file mode 100644
index 00000000000..d1814d506ad
--- /dev/null
+++ b/app/assets/javascripts/packages/details/store/getters.js
@@ -0,0 +1,115 @@
+import { generateConanRecipe } from '../utils';
+import { PackageType } from '../../shared/constants';
+import { getPackageTypeLabel } from '../../shared/utils';
+import { NpmManager } from '../constants';
+
+export const packagePipeline = ({ packageEntity }) => {
+ return packageEntity?.pipeline || null;
+};
+
+export const packageTypeDisplay = ({ packageEntity }) => {
+ return getPackageTypeLabel(packageEntity.package_type);
+};
+
+export const packageIcon = ({ packageEntity }) => {
+ if (packageEntity.package_type === PackageType.NUGET) {
+ return packageEntity.nuget_metadatum?.icon_url || null;
+ }
+
+ return null;
+};
+
+export const conanInstallationCommand = ({ packageEntity }) => {
+ const recipe = generateConanRecipe(packageEntity);
+
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `conan install ${recipe} --remote=gitlab`;
+};
+
+export const conanSetupCommand = ({ conanPath }) =>
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ `conan remote add gitlab ${conanPath}`;
+
+export const mavenInstallationXml = ({ packageEntity = {} }) => {
+ const {
+ app_group: appGroup = '',
+ app_name: appName = '',
+ app_version: appVersion = '',
+ } = packageEntity.maven_metadatum;
+
+ return `<dependency>
+ <groupId>${appGroup}</groupId>
+ <artifactId>${appName}</artifactId>
+ <version>${appVersion}</version>
+</dependency>`;
+};
+
+export const mavenInstallationCommand = ({ packageEntity = {} }) => {
+ const {
+ app_group: group = '',
+ app_name: name = '',
+ app_version: version = '',
+ } = packageEntity.maven_metadatum;
+
+ return `mvn dependency:get -Dartifact=${group}:${name}:${version}`;
+};
+
+export const mavenSetupXml = ({ mavenPath }) => `<repositories>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>${mavenPath}</url>
+ </repository>
+</repositories>
+
+<distributionManagement>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>${mavenPath}</url>
+ </repository>
+
+ <snapshotRepository>
+ <id>gitlab-maven</id>
+ <url>${mavenPath}</url>
+ </snapshotRepository>
+</distributionManagement>`;
+
+export const npmInstallationCommand = ({ packageEntity }) => (type = NpmManager.NPM) => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ const instruction = type === NpmManager.NPM ? 'npm i' : 'yarn add';
+
+ return `${instruction} ${packageEntity.name}`;
+};
+
+export const npmSetupCommand = ({ packageEntity, npmPath }) => (type = NpmManager.NPM) => {
+ const scope = packageEntity.name.substring(0, packageEntity.name.indexOf('/'));
+
+ if (type === NpmManager.NPM) {
+ return `echo ${scope}:registry=${npmPath} >> .npmrc`;
+ }
+
+ return `echo \\"${scope}:registry\\" \\"${npmPath}\\" >> .yarnrc`;
+};
+
+export const nugetInstallationCommand = ({ packageEntity }) =>
+ `nuget install ${packageEntity.name} -Source "GitLab"`;
+
+export const nugetSetupCommand = ({ nugetPath }) =>
+ `nuget source Add -Name "GitLab" -Source "${nugetPath}" -UserName <your_username> -Password <your_token>`;
+
+export const pypiPipCommand = ({ pypiPath, packageEntity }) =>
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ `pip install ${packageEntity.name} --index-url ${pypiPath}`;
+
+export const pypiSetupCommand = ({ pypiSetupPath }) => `[gitlab]
+repository = ${pypiSetupPath}
+username = __token__
+password = <your personal access token>`;
+
+export const composerRegistryInclude = ({ composerPath }) => {
+ const base = { type: 'composer', url: composerPath };
+ return JSON.stringify(base);
+};
+export const composerPackageInclude = ({ packageEntity }) => {
+ const base = { [packageEntity.name]: packageEntity.version };
+ return JSON.stringify(base);
+};
diff --git a/app/assets/javascripts/packages/details/store/index.js b/app/assets/javascripts/packages/details/store/index.js
new file mode 100644
index 00000000000..9687eb98544
--- /dev/null
+++ b/app/assets/javascripts/packages/details/store/index.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import fetchPackageVersions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default (initialState = {}) =>
+ new Vuex.Store({
+ actions: {
+ fetchPackageVersions,
+ },
+ getters,
+ mutations,
+ state: {
+ isLoading: false,
+ ...initialState,
+ },
+ });
diff --git a/app/assets/javascripts/packages/details/store/mutation_types.js b/app/assets/javascripts/packages/details/store/mutation_types.js
new file mode 100644
index 00000000000..340d668819c
--- /dev/null
+++ b/app/assets/javascripts/packages/details/store/mutation_types.js
@@ -0,0 +1,2 @@
+export const SET_LOADING = 'SET_LOADING';
+export const SET_PACKAGE_VERSIONS = 'SET_PACKAGE_VERSIONS';
diff --git a/app/assets/javascripts/packages/details/store/mutations.js b/app/assets/javascripts/packages/details/store/mutations.js
new file mode 100644
index 00000000000..e113638311b
--- /dev/null
+++ b/app/assets/javascripts/packages/details/store/mutations.js
@@ -0,0 +1,14 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_LOADING](state, isLoading) {
+ state.isLoading = isLoading;
+ },
+
+ [types.SET_PACKAGE_VERSIONS](state, versions) {
+ state.packageEntity = {
+ ...state.packageEntity,
+ versions,
+ };
+ },
+};
diff --git a/app/assets/javascripts/packages/details/utils.js b/app/assets/javascripts/packages/details/utils.js
new file mode 100644
index 00000000000..454c83c9ccd
--- /dev/null
+++ b/app/assets/javascripts/packages/details/utils.js
@@ -0,0 +1,23 @@
+import { TrackingActions } from './constants';
+
+export const trackInstallationTabChange = {
+ methods: {
+ trackInstallationTabChange(tabIndex) {
+ const action = tabIndex === 0 ? TrackingActions.INSTALLATION : TrackingActions.REGISTRY_SETUP;
+ this.track(action, { label: this.trackingLabel });
+ },
+ },
+};
+
+export function generateConanRecipe(packageEntity = {}) {
+ const {
+ name = '',
+ version = '',
+ conan_metadatum: {
+ package_username: packageUsername = '',
+ package_channel: packageChannel = '',
+ } = {},
+ } = packageEntity;
+
+ return `${name}/${version}@${packageUsername}/${packageChannel}`;
+}
diff --git a/app/assets/javascripts/packages/list/coming_soon/helpers.js b/app/assets/javascripts/packages/list/coming_soon/helpers.js
new file mode 100644
index 00000000000..5b6a4b3aa87
--- /dev/null
+++ b/app/assets/javascripts/packages/list/coming_soon/helpers.js
@@ -0,0 +1,55 @@
+/**
+ * Context:
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/198524
+ * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29491
+ *
+ */
+
+/**
+ * Constants
+ *
+ * LABEL_NAMES - an array of labels to filter issues in the GraphQL query
+ * WORKFLOW_PREFIX - the prefix for workflow labels
+ * ACCEPTING_CONTRIBUTIONS_TITLE - the accepting contributions label
+ */
+export const LABEL_NAMES = ['Package::Coming soon'];
+const WORKFLOW_PREFIX = 'workflow::';
+const ACCEPTING_CONTRIBUTIONS_TITLE = 'accepting merge requests';
+
+const setScoped = (label, scoped) => (label ? { ...label, scoped } : label);
+
+/**
+ * Finds workflow:: scoped labels and returns the first or null.
+ * @param {Object[]} labels Labels from the issue
+ */
+export const findWorkflowLabel = (labels = []) =>
+ labels.find(l => l.title.toLowerCase().includes(WORKFLOW_PREFIX.toLowerCase()));
+
+/**
+ * Determines if an issue is accepting community contributions by checking if
+ * the "Accepting merge requests" label is present.
+ * @param {Object[]} labels
+ */
+export const findAcceptingContributionsLabel = (labels = []) =>
+ labels.find(l => l.title.toLowerCase() === ACCEPTING_CONTRIBUTIONS_TITLE.toLowerCase());
+
+/**
+ * Formats the GraphQL response into the format that the view template expects.
+ * @param {Object} data GraphQL response
+ */
+export const toViewModel = data => {
+ // This just flatterns the issues -> nodes and labels -> nodes hierarchy
+ // into an array of objects.
+ const issues = (data.project?.issues?.nodes || []).map(i => ({
+ ...i,
+ labels: (i.labels?.nodes || []).map(node => node),
+ }));
+
+ return issues.map(x => ({
+ ...x,
+ labels: [
+ setScoped(findWorkflowLabel(x.labels), true),
+ setScoped(findAcceptingContributionsLabel(x.labels), false),
+ ].filter(Boolean),
+ }));
+};
diff --git a/app/assets/javascripts/packages/list/coming_soon/packages_coming_soon.vue b/app/assets/javascripts/packages/list/coming_soon/packages_coming_soon.vue
new file mode 100644
index 00000000000..766402d3619
--- /dev/null
+++ b/app/assets/javascripts/packages/list/coming_soon/packages_coming_soon.vue
@@ -0,0 +1,172 @@
+<script>
+import {
+ GlAlert,
+ GlEmptyState,
+ GlIcon,
+ GlLabel,
+ GlLink,
+ GlSkeletonLoader,
+ GlSprintf,
+} from '@gitlab/ui';
+import { ApolloQuery } from 'vue-apollo';
+import Tracking from '~/tracking';
+import { TrackingActions } from '../../shared/constants';
+import { s__ } from '~/locale';
+import comingSoonIssuesQuery from './queries/issues.graphql';
+import { toViewModel, LABEL_NAMES } from './helpers';
+
+export default {
+ name: 'ComingSoon',
+ components: {
+ GlAlert,
+ GlEmptyState,
+ GlIcon,
+ GlLabel,
+ GlLink,
+ GlSkeletonLoader,
+ GlSprintf,
+ ApolloQuery,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ illustration: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ suggestedContributionsPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ labelNames: LABEL_NAMES,
+ };
+ },
+ },
+ mounted() {
+ this.track(TrackingActions.COMING_SOON_REQUESTED);
+ },
+ methods: {
+ onIssueLinkClick(issueIid, label) {
+ this.track(TrackingActions.COMING_SOON_LIST, {
+ label,
+ value: issueIid,
+ });
+ },
+ onDocsLinkClick() {
+ this.track(TrackingActions.COMING_SOON_HELP);
+ },
+ },
+ loadingRows: 5,
+ i18n: {
+ alertTitle: s__('PackageRegistry|Upcoming package managers'),
+ alertIntro: s__(
+ "PackageRegistry|Is your favorite package manager missing? We'd love your help in building first-class support for it into GitLab! %{contributionLinkStart}Visit the contribution documentation%{contributionLinkEnd} to learn more about how to build support for new package managers into GitLab. Below is a list of package managers that are on our radar.",
+ ),
+ emptyStateTitle: s__('PackageRegistry|No upcoming issues'),
+ emptyStateDescription: s__('PackageRegistry|There are no upcoming issues to display.'),
+ },
+ comingSoonIssuesQuery,
+ toViewModel,
+};
+</script>
+
+<template>
+ <apollo-query
+ :query="$options.comingSoonIssuesQuery"
+ :variables="variables"
+ :update="$options.toViewModel"
+ >
+ <template #default="{ result: { data }, isLoading }">
+ <div>
+ <gl-alert :title="$options.i18n.alertTitle" :dismissible="false" variant="tip">
+ <gl-sprintf :message="$options.i18n.alertIntro">
+ <template #contributionLink="{ content }">
+ <gl-link
+ :href="suggestedContributionsPath"
+ target="_blank"
+ @click="onDocsLinkClick"
+ >{{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ </div>
+
+ <div v-if="isLoading" class="gl-display-flex gl-flex-direction-column">
+ <gl-skeleton-loader
+ v-for="index in $options.loadingRows"
+ :key="index"
+ :width="1000"
+ :height="80"
+ preserve-aspect-ratio="xMinYMax meet"
+ >
+ <rect width="700" height="10" x="0" y="16" rx="4" />
+ <rect width="60" height="10" x="0" y="45" rx="4" />
+ <rect width="60" height="10" x="70" y="45" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+
+ <template v-else-if="data && data.length">
+ <div
+ v-for="issue in data"
+ :key="issue.iid"
+ data-testid="issue-row"
+ class="gl-responsive-table-row gl-flex-direction-column gl-align-items-baseline"
+ >
+ <div class="table-section section-100 gl-white-space-normal text-truncate">
+ <gl-link
+ data-testid="issue-title-link"
+ :href="issue.webUrl"
+ class="gl-text-gray-900 gl-font-weight-bold"
+ @click="onIssueLinkClick(issue.iid, issue.title)"
+ >
+ {{ issue.title }}
+ </gl-link>
+ </div>
+
+ <div class="table-section section-100 gl-white-space-normal mt-md-3">
+ <div class="gl-display-flex gl-text-gray-400">
+ <gl-icon name="issues" class="gl-mr-2" />
+ <gl-link
+ data-testid="issue-id-link"
+ :href="issue.webUrl"
+ class="gl-text-gray-400 gl-mr-5"
+ @click="onIssueLinkClick(issue.iid, issue.title)"
+ >#{{ issue.iid }}</gl-link
+ >
+
+ <div v-if="issue.milestone" class="gl-display-flex gl-align-items-center gl-mr-5">
+ <gl-icon name="clock" class="gl-mr-2" />
+ <span data-testid="milestone">{{ issue.milestone.title }}</span>
+ </div>
+
+ <gl-label
+ v-for="label in issue.labels"
+ :key="label.title"
+ class="gl-mr-3"
+ size="sm"
+ :background-color="label.color"
+ :title="label.title"
+ :scoped="Boolean(label.scoped)"
+ />
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <gl-empty-state v-else :title="$options.i18n.emptyStateTitle" :svg-path="illustration">
+ <template #description>
+ <p>{{ $options.i18n.emptyStateDescription }}</p>
+ </template>
+ </gl-empty-state>
+ </template>
+ </apollo-query>
+</template>
diff --git a/app/assets/javascripts/packages/list/coming_soon/queries/issues.graphql b/app/assets/javascripts/packages/list/coming_soon/queries/issues.graphql
new file mode 100644
index 00000000000..36c27d9ad70
--- /dev/null
+++ b/app/assets/javascripts/packages/list/coming_soon/queries/issues.graphql
@@ -0,0 +1,20 @@
+query getComingSoonIssues($projectPath: ID!, $labelNames: [String]) {
+ project(fullPath: $projectPath) {
+ issues(state: opened, labelName: $labelNames) {
+ nodes {
+ iid
+ title
+ webUrl
+ labels {
+ nodes {
+ title
+ color
+ }
+ }
+ milestone {
+ title
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/packages/list/components/packages_filter.vue b/app/assets/javascripts/packages/list/components/packages_filter.vue
new file mode 100644
index 00000000000..17398071217
--- /dev/null
+++ b/app/assets/javascripts/packages/list/components/packages_filter.vue
@@ -0,0 +1,21 @@
+<script>
+import { GlSearchBoxByClick } from '@gitlab/ui';
+import { mapActions } from 'vuex';
+
+export default {
+ components: {
+ GlSearchBoxByClick,
+ },
+ methods: {
+ ...mapActions(['setFilter']),
+ },
+};
+</script>
+
+<template>
+ <gl-search-box-by-click
+ :placeholder="s__('PackageRegistry|Filter by name')"
+ @submit="$emit('filter')"
+ @input="setFilter"
+ />
+</template>
diff --git a/app/assets/javascripts/packages/list/components/packages_list.vue b/app/assets/javascripts/packages/list/components/packages_list.vue
new file mode 100644
index 00000000000..b26c6087e14
--- /dev/null
+++ b/app/assets/javascripts/packages/list/components/packages_list.vue
@@ -0,0 +1,129 @@
+<script>
+import { mapState, mapGetters } from 'vuex';
+import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
+import Tracking from '~/tracking';
+import { s__ } from '~/locale';
+import { TrackingActions } from '../../shared/constants';
+import { packageTypeToTrackCategory } from '../../shared/utils';
+import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
+import PackagesListRow from '../../shared/components/package_list_row.vue';
+
+export default {
+ components: {
+ GlPagination,
+ GlModal,
+ GlSprintf,
+ PackagesListLoader,
+ PackagesListRow,
+ },
+ mixins: [Tracking.mixin()],
+ data() {
+ return {
+ itemToBeDeleted: null,
+ };
+ },
+ computed: {
+ ...mapState({
+ perPage: state => state.pagination.perPage,
+ totalItems: state => state.pagination.total,
+ page: state => state.pagination.page,
+ isGroupPage: state => state.config.isGroupPage,
+ isLoading: 'isLoading',
+ }),
+ ...mapGetters({ list: 'getList' }),
+ currentPage: {
+ get() {
+ return this.page;
+ },
+ set(value) {
+ this.$emit('page:changed', value);
+ },
+ },
+ isListEmpty() {
+ return !this.list || this.list.length === 0;
+ },
+ modalAction() {
+ return s__('PackageRegistry|Delete package');
+ },
+ deletePackageName() {
+ return this.itemToBeDeleted?.name ?? '';
+ },
+ tracking() {
+ const category = this.itemToBeDeleted
+ ? packageTypeToTrackCategory(this.itemToBeDeleted.package_type)
+ : undefined;
+ return {
+ category,
+ };
+ },
+ },
+ methods: {
+ setItemToBeDeleted(item) {
+ this.itemToBeDeleted = { ...item };
+ this.track(TrackingActions.REQUEST_DELETE_PACKAGE);
+ this.$refs.packageListDeleteModal.show();
+ },
+ deleteItemConfirmation() {
+ this.$emit('package:delete', this.itemToBeDeleted);
+ this.track(TrackingActions.DELETE_PACKAGE);
+ this.itemToBeDeleted = null;
+ },
+ deleteItemCanceled() {
+ this.track(TrackingActions.CANCEL_DELETE_PACKAGE);
+ this.itemToBeDeleted = null;
+ },
+ },
+ i18n: {
+ deleteModalContent: s__(
+ 'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
+ ),
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex flex-column">
+ <slot v-if="isListEmpty && !isLoading" name="empty-state"></slot>
+
+ <div v-else-if="isLoading">
+ <packages-list-loader :is-group="isGroupPage" />
+ </div>
+
+ <template v-else>
+ <div data-qa-selector="packages-table">
+ <packages-list-row
+ v-for="packageEntity in list"
+ :key="packageEntity.id"
+ :package-entity="packageEntity"
+ :package-link="packageEntity._links.web_path"
+ :is-group="isGroupPage"
+ @packageToDelete="setItemToBeDeleted"
+ />
+ </div>
+
+ <gl-pagination
+ v-model="currentPage"
+ :per-page="perPage"
+ :total-items="totalItems"
+ align="center"
+ class="w-100 mt-2"
+ />
+
+ <gl-modal
+ ref="packageListDeleteModal"
+ modal-id="confirm-delete-pacakge"
+ ok-variant="danger"
+ @ok="deleteItemConfirmation"
+ @cancel="deleteItemCanceled"
+ >
+ <template #modal-title>{{ modalAction }}</template>
+ <template #modal-ok>{{ modalAction }}</template>
+ <gl-sprintf :message="$options.i18n.deleteModalContent">
+ <template #name>
+ <strong>{{ deletePackageName }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-modal>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/list/components/packages_list_app.vue b/app/assets/javascripts/packages/list/components/packages_list_app.vue
new file mode 100644
index 00000000000..ef242ea5f75
--- /dev/null
+++ b/app/assets/javascripts/packages/list/components/packages_list_app.vue
@@ -0,0 +1,111 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import { GlEmptyState, GlTab, GlTabs, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import PackageFilter from './packages_filter.vue';
+import PackageList from './packages_list.vue';
+import PackageSort from './packages_sort.vue';
+import { PACKAGE_REGISTRY_TABS } from '../constants';
+import PackagesComingSoon from '../coming_soon/packages_coming_soon.vue';
+
+export default {
+ components: {
+ GlEmptyState,
+ GlTab,
+ GlTabs,
+ GlLink,
+ GlSprintf,
+ PackageFilter,
+ PackageList,
+ PackageSort,
+ PackagesComingSoon,
+ },
+ computed: {
+ ...mapState({
+ emptyListIllustration: state => state.config.emptyListIllustration,
+ emptyListHelpUrl: state => state.config.emptyListHelpUrl,
+ comingSoon: state => state.config.comingSoon,
+ filterQuery: state => state.filterQuery,
+ selectedType: state => state.selectedType,
+ }),
+ tabsToRender() {
+ return PACKAGE_REGISTRY_TABS;
+ },
+ },
+ mounted() {
+ this.requestPackagesList();
+ },
+ methods: {
+ ...mapActions(['requestPackagesList', 'requestDeletePackage', 'setSelectedType']),
+ onPageChanged(page) {
+ return this.requestPackagesList({ page });
+ },
+ onPackageDeleteRequest(item) {
+ return this.requestDeletePackage(item);
+ },
+ tabChanged(index) {
+ const selected = PACKAGE_REGISTRY_TABS[index];
+
+ if (selected !== this.selectedType) {
+ this.setSelectedType(selected);
+ this.requestPackagesList();
+ }
+ },
+ emptyStateTitle({ title, type }) {
+ if (this.filterQuery) {
+ return s__('PackageRegistry|Sorry, your filter produced no results');
+ }
+
+ if (type) {
+ return sprintf(s__('PackageRegistry|There are no %{packageType} packages yet'), {
+ packageType: title,
+ });
+ }
+
+ return s__('PackageRegistry|There are no packages yet');
+ },
+ },
+ i18n: {
+ widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
+ noResults: s__(
+ 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.',
+ ),
+ },
+};
+</script>
+
+<template>
+ <gl-tabs @input="tabChanged">
+ <template #tabs-end>
+ <div class="d-flex align-self-center ml-md-auto py-1 py-md-0">
+ <package-filter class="mr-1" @filter="requestPackagesList" />
+ <package-sort @sort:changed="requestPackagesList" />
+ </div>
+ </template>
+
+ <gl-tab v-for="(tab, index) in tabsToRender" :key="index" :title="tab.title">
+ <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
+ <template #empty-state>
+ <gl-empty-state :title="emptyStateTitle(tab)" :svg-path="emptyListIllustration">
+ <template #description>
+ <gl-sprintf v-if="filterQuery" :message="$options.i18n.widenFilters" />
+ <gl-sprintf v-else :message="$options.i18n.noResults">
+ <template #noPackagesLink="{content}">
+ <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
+ </template>
+ </package-list>
+ </gl-tab>
+
+ <gl-tab v-if="comingSoon" :title="__('Coming soon')" lazy>
+ <packages-coming-soon
+ :illustration="emptyListIllustration"
+ :project-path="comingSoon.projectPath"
+ :suggested-contributions-path="comingSoon.suggestedContributions"
+ />
+ </gl-tab>
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/packages/list/components/packages_sort.vue b/app/assets/javascripts/packages/list/components/packages_sort.vue
new file mode 100644
index 00000000000..fa8f4f39d54
--- /dev/null
+++ b/app/assets/javascripts/packages/list/components/packages_sort.vue
@@ -0,0 +1,60 @@
+<script>
+import { GlSorting, GlSortingItem } from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { ASCENDING_ODER, DESCENDING_ORDER } from '../constants';
+import getTableHeaders from '../utils';
+
+export default {
+ name: 'PackageSort',
+ components: {
+ GlSorting,
+ GlSortingItem,
+ },
+ computed: {
+ ...mapState({
+ isGroupPage: state => state.config.isGroupPage,
+ orderBy: state => state.sorting.orderBy,
+ sort: state => state.sorting.sort,
+ }),
+ sortText() {
+ const field = this.sortableFields.find(s => s.orderBy === this.orderBy);
+ return field ? field.label : '';
+ },
+ sortableFields() {
+ return getTableHeaders(this.isGroupPage);
+ },
+ isSortAscending() {
+ return this.sort === ASCENDING_ODER;
+ },
+ },
+ methods: {
+ ...mapActions(['setSorting']),
+ onDirectionChange() {
+ const sort = this.isSortAscending ? DESCENDING_ORDER : ASCENDING_ODER;
+ this.setSorting({ sort });
+ this.$emit('sort:changed');
+ },
+ onSortItemClick(item) {
+ this.setSorting({ orderBy: item });
+ this.$emit('sort:changed');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-sorting
+ :text="sortText"
+ :is-ascending="isSortAscending"
+ @sortDirectionChange="onDirectionChange"
+ >
+ <gl-sorting-item
+ v-for="item in sortableFields"
+ ref="packageListSortItem"
+ :key="item.key"
+ @click="onSortItemClick(item.orderBy)"
+ >
+ {{ item.label }}
+ </gl-sorting-item>
+ </gl-sorting>
+</template>
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
new file mode 100644
index 00000000000..510d04965cb
--- /dev/null
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -0,0 +1,101 @@
+import { __, s__ } from '~/locale';
+import { PackageType } from '../shared/constants';
+
+export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __(
+ 'Something went wrong while fetching the packages list.',
+);
+export const FETCH_PACKAGE_ERROR_MESSAGE = __('Something went wrong while fetching the package.');
+export const DELETE_PACKAGE_ERROR_MESSAGE = __('Something went wrong while deleting the package.');
+export const DELETE_PACKAGE_SUCCESS_MESSAGE = __('Package deleted successfully');
+
+export const DEFAULT_PAGE = 1;
+export const DEFAULT_PAGE_SIZE = 20;
+
+export const GROUP_PAGE_TYPE = 'groups';
+
+export const LIST_KEY_NAME = 'name';
+export const LIST_KEY_PROJECT = 'project_path';
+export const LIST_KEY_VERSION = 'version';
+export const LIST_KEY_PACKAGE_TYPE = 'package_type';
+export const LIST_KEY_CREATED_AT = 'created_at';
+export const LIST_KEY_ACTIONS = 'actions';
+
+export const LIST_LABEL_NAME = __('Name');
+export const LIST_LABEL_PROJECT = __('Project');
+export const LIST_LABEL_VERSION = __('Version');
+export const LIST_LABEL_PACKAGE_TYPE = __('Type');
+export const LIST_LABEL_CREATED_AT = __('Created');
+export const LIST_LABEL_ACTIONS = '';
+
+export const LIST_ORDER_BY_PACKAGE_TYPE = 'type';
+
+export const ASCENDING_ODER = 'asc';
+export const DESCENDING_ORDER = 'desc';
+
+// The following is not translated because it is used to build a JavaScript exception error message
+export const MISSING_DELETE_PATH_ERROR = 'Missing delete_api_path link';
+
+export const TABLE_HEADER_FIELDS = [
+ {
+ key: LIST_KEY_NAME,
+ label: LIST_LABEL_NAME,
+ orderBy: LIST_KEY_NAME,
+ class: ['text-left'],
+ },
+ {
+ key: LIST_KEY_PROJECT,
+ label: LIST_LABEL_PROJECT,
+ orderBy: LIST_KEY_PROJECT,
+ class: ['text-left'],
+ },
+ {
+ key: LIST_KEY_VERSION,
+ label: LIST_LABEL_VERSION,
+ orderBy: LIST_KEY_VERSION,
+ class: ['text-center'],
+ },
+ {
+ key: LIST_KEY_PACKAGE_TYPE,
+ label: LIST_LABEL_PACKAGE_TYPE,
+ orderBy: LIST_ORDER_BY_PACKAGE_TYPE,
+ class: ['text-center'],
+ },
+ {
+ key: LIST_KEY_CREATED_AT,
+ label: LIST_LABEL_CREATED_AT,
+ orderBy: LIST_KEY_CREATED_AT,
+ class: ['text-center'],
+ },
+];
+
+export const PACKAGE_REGISTRY_TABS = [
+ {
+ title: __('All'),
+ type: null,
+ },
+ {
+ title: s__('PackageRegistry|Composer'),
+ type: PackageType.COMPOSER,
+ },
+ {
+ title: s__('PackageRegistry|Conan'),
+ type: PackageType.CONAN,
+ },
+
+ {
+ title: s__('PackageRegistry|Maven'),
+ type: PackageType.MAVEN,
+ },
+ {
+ title: s__('PackageRegistry|NPM'),
+ type: PackageType.NPM,
+ },
+ {
+ title: s__('PackageRegistry|NuGet'),
+ type: PackageType.NUGET,
+ },
+ {
+ title: s__('PackageRegistry|PyPi'),
+ type: PackageType.PYPI,
+ },
+];
diff --git a/app/assets/javascripts/packages/list/packages_list_app_bundle.js b/app/assets/javascripts/packages/list/packages_list_app_bundle.js
new file mode 100644
index 00000000000..2f240cff143
--- /dev/null
+++ b/app/assets/javascripts/packages/list/packages_list_app_bundle.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import Translate from '~/vue_shared/translate';
+import { createStore } from './stores';
+import PackagesListApp from './components/packages_list_app.vue';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+Vue.use(Translate);
+
+export default () => {
+ const el = document.getElementById('js-vue-packages-list');
+ const store = createStore();
+ store.dispatch('setInitialState', el.dataset);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el,
+ store,
+ apolloProvider,
+ components: {
+ PackagesListApp,
+ },
+ render(createElement) {
+ return createElement('packages-list-app');
+ },
+ });
+};
diff --git a/app/assets/javascripts/packages/list/stores/actions.js b/app/assets/javascripts/packages/list/stores/actions.js
new file mode 100644
index 00000000000..0ed24aee2c5
--- /dev/null
+++ b/app/assets/javascripts/packages/list/stores/actions.js
@@ -0,0 +1,73 @@
+import Api from '~/api';
+import axios from '~/lib/utils/axios_utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import * as types from './mutation_types';
+import {
+ FETCH_PACKAGES_LIST_ERROR_MESSAGE,
+ DELETE_PACKAGE_ERROR_MESSAGE,
+ DELETE_PACKAGE_SUCCESS_MESSAGE,
+ DEFAULT_PAGE,
+ DEFAULT_PAGE_SIZE,
+ MISSING_DELETE_PATH_ERROR,
+} from '../constants';
+import { getNewPaginationPage } from '../utils';
+
+export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
+export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data);
+export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data);
+export const setSelectedType = ({ commit }, data) => commit(types.SET_SELECTED_TYPE, data);
+export const setFilter = ({ commit }, data) => commit(types.SET_FILTER, data);
+
+export const receivePackagesListSuccess = ({ commit }, { data, headers }) => {
+ commit(types.SET_PACKAGE_LIST_SUCCESS, data);
+ commit(types.SET_PAGINATION, headers);
+};
+
+export const requestPackagesList = ({ dispatch, state }, params = {}) => {
+ dispatch('setLoading', true);
+
+ const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params;
+ const { sort, orderBy } = state.sorting;
+
+ const type = state.selectedType?.type?.toLowerCase();
+ const nameFilter = state.filterQuery?.toLowerCase();
+ const packageFilters = { package_type: type, package_name: nameFilter };
+
+ const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages';
+
+ return Api[apiMethod](state.config.resourceId, {
+ params: { page, per_page, sort, order_by: orderBy, ...packageFilters },
+ })
+ .then(({ data, headers }) => {
+ dispatch('receivePackagesListSuccess', { data, headers });
+ })
+ .catch(() => {
+ createFlash(FETCH_PACKAGES_LIST_ERROR_MESSAGE);
+ })
+ .finally(() => {
+ dispatch('setLoading', false);
+ });
+};
+
+export const requestDeletePackage = ({ dispatch, state }, { _links }) => {
+ if (!_links || !_links.delete_api_path) {
+ createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
+ const error = new Error(MISSING_DELETE_PATH_ERROR);
+ return Promise.reject(error);
+ }
+
+ dispatch('setLoading', true);
+ return axios
+ .delete(_links.delete_api_path)
+ .then(() => {
+ const { page: currentPage, perPage, total } = state.pagination;
+ const page = getNewPaginationPage(currentPage, perPage, total - 1);
+
+ dispatch('requestPackagesList', { page });
+ createFlash(DELETE_PACKAGE_SUCCESS_MESSAGE, 'success');
+ })
+ .catch(() => {
+ dispatch('setLoading', false);
+ createFlash(DELETE_PACKAGE_ERROR_MESSAGE);
+ });
+};
diff --git a/app/assets/javascripts/packages/list/stores/getters.js b/app/assets/javascripts/packages/list/stores/getters.js
new file mode 100644
index 00000000000..0af7e453f19
--- /dev/null
+++ b/app/assets/javascripts/packages/list/stores/getters.js
@@ -0,0 +1,5 @@
+import { LIST_KEY_PROJECT } from '../constants';
+import { beautifyPath } from '../../shared/utils';
+
+export default state =>
+ state.packages.map(p => ({ ...p, projectPathName: beautifyPath(p[LIST_KEY_PROJECT]) }));
diff --git a/app/assets/javascripts/packages/list/stores/index.js b/app/assets/javascripts/packages/list/stores/index.js
new file mode 100644
index 00000000000..1d6a4bf831d
--- /dev/null
+++ b/app/assets/javascripts/packages/list/stores/index.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import getList from './getters';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ state,
+ getters: {
+ getList,
+ },
+ actions,
+ mutations,
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/packages/list/stores/mutation_types.js b/app/assets/javascripts/packages/list/stores/mutation_types.js
new file mode 100644
index 00000000000..a5a584ccf1f
--- /dev/null
+++ b/app/assets/javascripts/packages/list/stores/mutation_types.js
@@ -0,0 +1,8 @@
+export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
+
+export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS';
+export const SET_PAGINATION = 'SET_PAGINATION';
+export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
+export const SET_SORTING = 'SET_SORTING';
+export const SET_SELECTED_TYPE = 'SET_SELECTED_TYPE';
+export const SET_FILTER = 'SET_FILTER';
diff --git a/app/assets/javascripts/packages/list/stores/mutations.js b/app/assets/javascripts/packages/list/stores/mutations.js
new file mode 100644
index 00000000000..a47ba356c0a
--- /dev/null
+++ b/app/assets/javascripts/packages/list/stores/mutations.js
@@ -0,0 +1,45 @@
+import * as types from './mutation_types';
+import {
+ parseIntPagination,
+ normalizeHeaders,
+ convertObjectPropsToCamelCase,
+} from '~/lib/utils/common_utils';
+import { GROUP_PAGE_TYPE } from '../constants';
+
+export default {
+ [types.SET_INITIAL_STATE](state, config) {
+ const { comingSoonJson, ...rest } = config;
+ const comingSoonObj = JSON.parse(comingSoonJson);
+
+ state.config = {
+ ...rest,
+ comingSoon: comingSoonObj && convertObjectPropsToCamelCase(comingSoonObj),
+ isGroupPage: config.pageType === GROUP_PAGE_TYPE,
+ };
+ },
+
+ [types.SET_PACKAGE_LIST_SUCCESS](state, packages) {
+ state.packages = packages;
+ },
+
+ [types.SET_MAIN_LOADING](state, isLoading) {
+ state.isLoading = isLoading;
+ },
+
+ [types.SET_PAGINATION](state, headers) {
+ const normalizedHeaders = normalizeHeaders(headers);
+ state.pagination = parseIntPagination(normalizedHeaders);
+ },
+
+ [types.SET_SORTING](state, sorting) {
+ state.sorting = { ...state.sorting, ...sorting };
+ },
+
+ [types.SET_SELECTED_TYPE](state, type) {
+ state.selectedType = type;
+ },
+
+ [types.SET_FILTER](state, query) {
+ state.filterQuery = query;
+ },
+};
diff --git a/app/assets/javascripts/packages/list/stores/state.js b/app/assets/javascripts/packages/list/stores/state.js
new file mode 100644
index 00000000000..18ab2390b87
--- /dev/null
+++ b/app/assets/javascripts/packages/list/stores/state.js
@@ -0,0 +1,57 @@
+import { PACKAGE_REGISTRY_TABS } from '../constants';
+
+export default () => ({
+ /**
+ * Determine if the component is loading data from the API
+ */
+ isLoading: false,
+ /**
+ * configuration object, set once at store creation with the following structure
+ * {
+ * resourceId: String,
+ * pageType: String,
+ * emptyListIllustration: String,
+ * emptyListHelpUrl: String,
+ * comingSoon: { projectPath: String, suggestedContributions : String } | null;
+ * }
+ */
+ config: {},
+ /**
+ * Each object in `packages` has the following structure:
+ * {
+ * id: String
+ * name: String,
+ * version: String,
+ * package_type: String // endpoint to request the list
+ * }
+ */
+ packages: [],
+ /**
+ * Pagination object has the following structure:
+ * {
+ * perPage: Number,
+ * page: Number
+ * total: Number
+ * }
+ */
+ pagination: {},
+ /**
+ * Sorting object has the following structure:
+ * {
+ * sort: String,
+ * orderBy: String
+ * }
+ */
+ sorting: {
+ sort: 'desc',
+ orderBy: 'created_at',
+ },
+ /**
+ * The search query that is used to filter packages by name
+ */
+ filterQuery: '',
+ /**
+ * The selected TAB of the package types tabs
+ */
+ selectedType: PACKAGE_REGISTRY_TABS[0],
+});
diff --git a/app/assets/javascripts/packages/list/utils.js b/app/assets/javascripts/packages/list/utils.js
new file mode 100644
index 00000000000..98d78db8706
--- /dev/null
+++ b/app/assets/javascripts/packages/list/utils.js
@@ -0,0 +1,25 @@
+import { LIST_KEY_PROJECT, TABLE_HEADER_FIELDS } from './constants';
+
+export default isGroupPage =>
+ TABLE_HEADER_FIELDS.filter(f => f.key !== LIST_KEY_PROJECT || isGroupPage);
+
+/**
+ * A small util function that works out if the delete action has deleted the
+ * last item on the current paginated page and if so, returns the previous
+ * page. This ensures the user won't end up on an empty paginated page.
+ *
+ * @param {number} currentPage The current page the user is on
+ * @param {number} perPage Number of items to display per page
+ * @param {number} totalPackages The total number of items
+ */
+export const getNewPaginationPage = (currentPage, perPage, totalItems) => {
+ if (totalItems <= perPage) {
+ return 1;
+ }
+
+ if (currentPage > 1 && (currentPage - 1) * perPage >= totalItems) {
+ return currentPage - 1;
+ }
+
+ return currentPage;
+};
diff --git a/app/assets/javascripts/packages/shared/components/package_list_row.vue b/app/assets/javascripts/packages/shared/components/package_list_row.vue
new file mode 100644
index 00000000000..e000279b794
--- /dev/null
+++ b/app/assets/javascripts/packages/shared/components/package_list_row.vue
@@ -0,0 +1,139 @@
+<script>
+import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import PackageTags from './package_tags.vue';
+import PublishMethod from './publish_method.vue';
+import { getPackageTypeLabel } from '../utils';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+
+export default {
+ name: 'PackageListRow',
+ components: {
+ GlButton,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ PackageTags,
+ PublishMethod,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ packageLink: {
+ type: String,
+ required: true,
+ },
+ disableDelete: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ isGroup: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ showPackageType: {
+ type: Boolean,
+ default: true,
+ required: false,
+ },
+ },
+ computed: {
+ packageType() {
+ return getPackageTypeLabel(this.packageEntity.package_type);
+ },
+ hasPipeline() {
+ return Boolean(this.packageEntity.pipeline);
+ },
+ hasProjectLink() {
+ return Boolean(this.packageEntity.project_path);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-responsive-table-row" data-qa-selector="packages-row">
+ <div class="table-section section-50 d-flex flex-md-column justify-content-between flex-wrap">
+ <div class="d-flex align-items-center mr-2">
+ <gl-link
+ :href="packageLink"
+ data-qa-selector="package_link"
+ class="text-dark font-weight-bold mb-md-1"
+ >
+ {{ packageEntity.name }}
+ </gl-link>
+
+ <package-tags
+ v-if="packageEntity.tags && packageEntity.tags.length"
+ class="gl-ml-3"
+ :tags="packageEntity.tags"
+ hide-label
+ :tag-display-limit="1"
+ />
+ </div>
+
+ <div class="d-flex text-secondary text-truncate mt-md-2">
+ <span>{{ packageEntity.version }}</span>
+
+ <div v-if="hasPipeline" class="d-none d-md-inline-block ml-1">
+ <gl-sprintf :message="s__('PackageRegistry|published by %{author}')">
+ <template #author>{{ packageEntity.pipeline.user.name }}</template>
+ </gl-sprintf>
+ </div>
+
+ <div v-if="hasProjectLink" class="d-flex align-items-center">
+ <gl-icon name="review-list" class="text-secondary ml-2 mr-1" />
+
+ <gl-link
+ data-testid="packages-row-project"
+ :href="`/${packageEntity.project_path}`"
+ class="text-secondary"
+ >{{ packageEntity.projectPathName }}</gl-link
+ >
+ </div>
+
+ <div v-if="showPackageType" class="d-flex align-items-center" data-testid="package-type">
+ <gl-icon name="package" class="text-secondary ml-2 mr-1" />
+ <span>{{ packageType }}</span>
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="table-section d-flex flex-md-column justify-content-between align-items-md-end"
+ :class="disableDelete ? 'section-50' : 'section-40'"
+ >
+ <publish-method :package-entity="packageEntity" :is-group="isGroup" />
+
+ <div class="text-secondary order-0 order-md-1 mt-md-2">
+ <gl-sprintf :message="__('Created %{timestamp}')">
+ <template #timestamp>
+ <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)">
+ {{ timeFormatted(packageEntity.created_at) }}
+ </span>
+ </template>
+ </gl-sprintf>
+ </div>
+ </div>
+
+ <div v-if="!disableDelete" class="table-section section-10 d-flex justify-content-end">
+ <gl-button
+ data-testid="action-delete"
+ icon="remove"
+ category="primary"
+ variant="danger"
+ :title="s__('PackageRegistry|Remove package')"
+ :aria-label="s__('PackageRegistry|Remove package')"
+ :disabled="!packageEntity._links.delete_api_path"
+ @click="$emit('packageToDelete', packageEntity)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/shared/components/package_tags.vue b/app/assets/javascripts/packages/shared/components/package_tags.vue
new file mode 100644
index 00000000000..391f53c225b
--- /dev/null
+++ b/app/assets/javascripts/packages/shared/components/package_tags.vue
@@ -0,0 +1,108 @@
+<script>
+import { GlBadge, GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { n__ } from '~/locale';
+
+export default {
+ name: 'PackageTags',
+ components: {
+ GlBadge,
+ GlIcon,
+ GlSprintf,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ tagDisplayLimit: {
+ type: Number,
+ required: false,
+ default: 2,
+ },
+ tags: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ hideLabel: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ tagCount() {
+ return this.tags.length;
+ },
+ tagsToRender() {
+ return this.tags.slice(0, this.tagDisplayLimit);
+ },
+ moreTagsDisplay() {
+ return Math.max(0, this.tags.length - this.tagDisplayLimit);
+ },
+ moreTagsTooltip() {
+ if (this.moreTagsDisplay) {
+ return this.tags
+ .slice(this.tagDisplayLimit)
+ .map(x => x.name)
+ .join(', ');
+ }
+
+ return '';
+ },
+ tagsDisplay() {
+ return n__('%d tag', '%d tags', this.tagCount);
+ },
+ },
+ methods: {
+ tagBadgeClass(index) {
+ return {
+ 'gl-display-none': true,
+ 'gl-display-flex': this.tagCount === 1,
+ 'd-md-flex': this.tagCount > 1,
+ 'gl-mr-2': index !== this.tagsToRender.length - 1,
+ 'gl-ml-3': !this.hideLabel && index === 0,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center">
+ <div v-if="!hideLabel" data-testid="tagLabel" class="gl-display-flex gl-align-items-center">
+ <gl-icon name="labels" class="gl-text-gray-500 gl-mr-3" />
+ <span class="gl-font-weight-bold">{{ tagsDisplay }}</span>
+ </div>
+
+ <gl-badge
+ v-for="(tag, index) in tagsToRender"
+ :key="index"
+ data-testid="tagBadge"
+ :class="tagBadgeClass(index)"
+ variant="info"
+ >{{ tag.name }}</gl-badge
+ >
+
+ <gl-badge
+ v-if="moreTagsDisplay"
+ v-gl-tooltip
+ data-testid="moreBadge"
+ variant="muted"
+ :title="moreTagsTooltip"
+ class="gl-display-none d-md-flex gl-ml-2"
+ ><gl-sprintf :message="__('+%{tags} more')">
+ <template #tags>
+ {{ moreTagsDisplay }}
+ </template>
+ </gl-sprintf></gl-badge
+ >
+
+ <gl-badge
+ v-if="moreTagsDisplay && hideLabel"
+ data-testid="moreBadge"
+ variant="muted"
+ class="d-md-none gl-ml-2"
+ >{{ tagsDisplay }}</gl-badge
+ >
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/shared/components/packages_list_loader.vue b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
new file mode 100644
index 00000000000..cd9ef74d467
--- /dev/null
+++ b/app/assets/javascripts/packages/shared/components/packages_list_loader.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+ props: {
+ isGroup: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ desktopShapes() {
+ return this.isGroup ? this.$options.shapes.groups : this.$options.shapes.projects;
+ },
+ desktopHeight() {
+ return this.isGroup ? 38 : 54;
+ },
+ mobileHeight() {
+ return this.isGroup ? 160 : 170;
+ },
+ },
+ shapes: {
+ groups: [
+ { type: 'rect', width: '100', height: '10', x: '0', y: '15' },
+ { type: 'rect', width: '100', height: '10', x: '195', y: '15' },
+ { type: 'rect', width: '60', height: '10', x: '475', y: '15' },
+ { type: 'rect', width: '60', height: '10', x: '675', y: '15' },
+ { type: 'rect', width: '100', height: '10', x: '900', y: '15' },
+ ],
+ projects: [
+ { type: 'rect', width: '220', height: '10', x: '0', y: '20' },
+ { type: 'rect', width: '60', height: '10', x: '305', y: '20' },
+ { type: 'rect', width: '60', height: '10', x: '535', y: '20' },
+ { type: 'rect', width: '100', height: '10', x: '760', y: '20' },
+ { type: 'rect', width: '30', height: '30', x: '970', y: '10', ref: 'button-loader' },
+ ],
+ },
+ rowsToRender: {
+ mobile: 5,
+ desktop: 20,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="d-xs-flex flex-column d-md-none">
+ <gl-skeleton-loader
+ v-for="index in $options.rowsToRender.mobile"
+ :key="index"
+ :width="500"
+ :height="mobileHeight"
+ preserve-aspect-ratio="xMinYMax meet"
+ >
+ <rect width="500" height="10" x="0" y="15" rx="4" />
+ <rect width="500" height="10" x="0" y="45" rx="4" />
+ <rect width="500" height="10" x="0" y="75" rx="4" />
+ <rect width="500" height="10" x="0" y="105" rx="4" />
+ <rect v-if="isGroup" width="500" height="10" x="0" y="135" rx="4" />
+ <rect v-else width="30" height="30" x="470" y="135" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+
+ <div class="d-none d-md-flex flex-column">
+ <gl-skeleton-loader
+ v-for="index in $options.rowsToRender.desktop"
+ :key="index"
+ :width="1000"
+ :height="desktopHeight"
+ preserve-aspect-ratio="xMinYMax meet"
+ >
+ <component
+ :is="r.type"
+ v-for="(r, rIndex) in desktopShapes"
+ :key="rIndex"
+ rx="4"
+ v-bind="r"
+ />
+ </gl-skeleton-loader>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/shared/components/publish_method.vue b/app/assets/javascripts/packages/shared/components/publish_method.vue
new file mode 100644
index 00000000000..1e18562a421
--- /dev/null
+++ b/app/assets/javascripts/packages/shared/components/publish_method.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { getCommitLink } from '../utils';
+
+export default {
+ name: 'PublishMethod',
+ components: {
+ ClipboardButton,
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ isGroup: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ hasPipeline() {
+ return Boolean(this.packageEntity.pipeline);
+ },
+ packageShaShort() {
+ return this.packageEntity.pipeline?.sha.substring(0, 8);
+ },
+ linkToCommit() {
+ return getCommitLink(this.packageEntity, this.isGroup);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex align-items-center text-secondary order-1 order-md-0 mb-md-1">
+ <template v-if="hasPipeline">
+ <gl-icon name="git-merge" class="mr-1" />
+ <strong ref="pipeline-ref" class="mr-1 text-dark">{{ packageEntity.pipeline.ref }}</strong>
+
+ <gl-icon name="commit" class="mr-1" />
+ <gl-link ref="pipeline-sha" :href="linkToCommit" class="mr-1">{{ packageShaShort }}</gl-link>
+
+ <clipboard-button
+ :text="packageEntity.pipeline.sha"
+ :title="__('Copy commit SHA')"
+ css-class="border-0 text-secondary py-0 px-1"
+ />
+ </template>
+
+ <template v-else>
+ <gl-icon name="upload" class="mr-1" />
+ <strong ref="manual-ref" class="text-dark">{{
+ s__('PackageRegistry|Manually Published')
+ }}</strong>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js
new file mode 100644
index 00000000000..279c2959fa9
--- /dev/null
+++ b/app/assets/javascripts/packages/shared/constants.js
@@ -0,0 +1,24 @@
+export const PackageType = {
+ CONAN: 'conan',
+ MAVEN: 'maven',
+ NPM: 'npm',
+ NUGET: 'nuget',
+ PYPI: 'pypi',
+ COMPOSER: 'composer',
+};
+
+export const TrackingActions = {
+ DELETE_PACKAGE: 'delete_package',
+ REQUEST_DELETE_PACKAGE: 'request_delete_package',
+ CANCEL_DELETE_PACKAGE: 'cancel_delete_package',
+ PULL_PACKAGE: 'pull_package',
+ COMING_SOON_REQUESTED: 'activate_coming_soon_requested',
+ COMING_SOON_LIST: 'click_coming_soon_issue_link',
+ COMING_SOON_HELP: 'click_coming_soon_documentation_link',
+};
+
+export const TrackingCategories = {
+ [PackageType.MAVEN]: 'MavenPackages',
+ [PackageType.NPM]: 'NpmPackages',
+ [PackageType.CONAN]: 'ConanPackages',
+};
diff --git a/app/assets/javascripts/packages/shared/utils.js b/app/assets/javascripts/packages/shared/utils.js
new file mode 100644
index 00000000000..a0c7389651d
--- /dev/null
+++ b/app/assets/javascripts/packages/shared/utils.js
@@ -0,0 +1,36 @@
+import { s__ } from '~/locale';
+import { PackageType, TrackingCategories } from './constants';
+
+export const packageTypeToTrackCategory = type =>
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ `UI::${TrackingCategories[type]}`;
+
+export const beautifyPath = path => (path ? path.split('/').join(' / ') : '');
+
+export const getPackageTypeLabel = packageType => {
+ switch (packageType) {
+ case PackageType.CONAN:
+ return s__('PackageType|Conan');
+ case PackageType.MAVEN:
+ return s__('PackageType|Maven');
+ case PackageType.NPM:
+ return s__('PackageType|NPM');
+ case PackageType.NUGET:
+ return s__('PackageType|NuGet');
+ case PackageType.PYPI:
+ return s__('PackageType|PyPi');
+ case PackageType.COMPOSER:
+ return s__('PackageType|Composer');
+
+ default:
+ return null;
+ }
+};
+
+export const getCommitLink = ({ project_path: projectPath, pipeline = {} }, isGroup = false) => {
+ if (isGroup) {
+ return `/${projectPath}/commit/${pipeline.sha}`;
+ }
+
+ return `../commit/${pipeline.sha}`;
+};
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 413045d960e..e7b468f039f 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
@@ -1,6 +1,6 @@
import axios from '../../../lib/utils/axios_utils';
import { __ } from '../../../locale';
-import flash from '../../../flash';
+import { deprecatedCreateFlash as flash } from '../../../flash';
export default class PayloadPreviewer {
constructor(trigger, container) {
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 f64e0bbbfda..a75f5d318a0 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import { textColorForBackground } from '~/lib/utils/color_utils';
diff --git a/app/assets/javascripts/pages/admin/clusters/show/index.js b/app/assets/javascripts/pages/admin/clusters/show/index.js
index ccf631b2c53..f87da6c7074 100644
--- a/app/assets/javascripts/pages/admin/clusters/show/index.js
+++ b/app/assets/javascripts/pages/admin/clusters/show/index.js
@@ -1,7 +1,9 @@
import ClustersBundle from '~/clusters/clusters_bundle';
import initClusterHealth from '~/pages/projects/clusters/show/cluster_health';
+import initIntegrationForm from '~/clusters/forms/show';
document.addEventListener('DOMContentLoaded', () => {
new ClustersBundle(); // eslint-disable-line no-new
initClusterHealth();
+ initIntegrationForm();
});
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 eb03baf4894..120512bf15e 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 axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
diff --git a/app/assets/javascripts/pages/admin/runners/index.js b/app/assets/javascripts/pages/admin/runners/index.js
index ce8fd18b6a2..e60c6133c7c 100644
--- a/app/assets/javascripts/pages/admin/runners/index.js
+++ b/app/assets/javascripts/pages/admin/runners/index.js
@@ -6,5 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ADMIN_RUNNERS,
filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys,
+ useDefaultState: true,
});
});
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 71df677c7fd..e09b8e1bdd5 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -1,12 +1,12 @@
<script>
import { escape } from 'lodash';
-import { GlModal, GlDeprecatedButton, GlFormInput } from '@gitlab/ui';
+import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
components: {
GlModal,
- GlDeprecatedButton,
+ GlButton,
GlFormInput,
},
props: {
@@ -122,15 +122,18 @@ export default {
</form>
</template>
<template #modal-footer>
- <gl-deprecated-button variant="secondary" @click="onCancel">{{
- s__('Cancel')
- }}</gl-deprecated-button>
- <gl-deprecated-button :disabled="!canSubmit" variant="warning" @click="onSecondaryAction">
+ <gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button>
+ <gl-button
+ :disabled="!canSubmit"
+ category="primary"
+ variant="warning"
+ @click="onSecondaryAction"
+ >
{{ secondaryAction }}
- </gl-deprecated-button>
- <gl-deprecated-button :disabled="!canSubmit" variant="danger" @click="onSubmit">{{
+ </gl-button>
+ <gl-button :disabled="!canSubmit" category="primary" variant="danger" @click="onSubmit">{{
action
- }}</gl-deprecated-button>
+ }}</gl-button>
</template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js
index 2ffeed8a584..71cdaf45052 100644
--- a/app/assets/javascripts/pages/dashboard/issues/index.js
+++ b/app/assets/javascripts/pages/dashboard/issues/index.js
@@ -8,6 +8,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ useDefaultState: true,
});
projectSelect();
diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
index 24d7b592948..10df18c85e7 100644
--- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js
+++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
@@ -10,6 +10,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ useDefaultState: true,
});
projectSelect();
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
new file mode 100644
index 00000000000..6b907f31a37
--- /dev/null
+++ b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlBanner } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ components: {
+ GlBanner,
+ },
+ inject: {
+ svgPath: {
+ default: '',
+ },
+ preferencesBehaviorPath: {
+ default: '',
+ },
+ calloutsPath: {
+ default: '',
+ },
+ calloutsFeatureId: {
+ default: '',
+ },
+ },
+ i18n: {
+ title: s__('CustomizeHomepageBanner|Do you want to customize this page?'),
+ body: s__(
+ 'CustomizeHomepageBanner|This page shows a list of your projects by default but it can be changed to show projects\' activity, groups, your to-do list, assigned issues, assigned merge requests, and more. You can change this under "Homepage content" in your preferences',
+ ),
+ button_text: s__('CustomizeHomepageBanner|Go to preferences'),
+ },
+ data() {
+ return {
+ visible: true,
+ };
+ },
+ methods: {
+ handleClose() {
+ axios
+ .post(this.calloutsPath, {
+ feature_name: this.calloutsFeatureId,
+ })
+ .catch(e => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings, no-console
+ console.error('Failed to dismiss banner.', e);
+ });
+
+ this.visible = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-banner
+ v-if="visible"
+ :title="$options.i18n.title"
+ :button-text="$options.i18n.button_text"
+ :button-link="preferencesBehaviorPath"
+ :svg-path="svgPath"
+ @close="handleClose"
+ >
+ <p>
+ {{ $options.i18n.body }}
+ </p>
+ </gl-banner>
+</template>
diff --git a/app/assets/javascripts/pages/dashboard/projects/index.js b/app/assets/javascripts/pages/dashboard/projects/index/index.js
index 01001d4f3ff..b3c95f4ac1f 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index.js
+++ b/app/assets/javascripts/pages/dashboard/projects/index/index.js
@@ -1,5 +1,8 @@
import ProjectsList from '~/projects_list';
+import initCustomizeHomepageBanner from './init_customize_homepage_banner';
document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
+
+ initCustomizeHomepageBanner();
});
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js b/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js
new file mode 100644
index 00000000000..c0735dde1da
--- /dev/null
+++ b/app/assets/javascripts/pages/dashboard/projects/index/init_customize_homepage_banner.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import CustomizeHomepageBanner from './components/customize_homepage_banner.vue';
+
+export default () => {
+ const el = document.querySelector('.js-customize-homepage-banner');
+
+ if (!el) {
+ return false;
+ }
+
+ return new Vue({
+ el,
+ provide: { ...el.dataset },
+ render: createElement => createElement(CustomizeHomepageBanner),
+ });
+};
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 5230bdf9cdd..f76b4b44452 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -7,7 +7,7 @@ import UsersSelect from '~/users_select';
import { isMetaClick } from '~/lib/utils/common_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
export default class Todos {
diff --git a/app/assets/javascripts/pages/groups/clusters/index.js b/app/assets/javascripts/pages/groups/clusters/index.js
index 4d04c37caa7..9f466e0d60a 100644
--- a/app/assets/javascripts/pages/groups/clusters/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/index.js
@@ -1,5 +1,7 @@
import initCreateCluster from '~/create_cluster/init_create_cluster';
+import initIntegrationForm from '~/clusters/forms/show/index';
document.addEventListener('DOMContentLoaded', () => {
initCreateCluster(document, gon);
+ initIntegrationForm();
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 4f15f5ec58c..2496003919a 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -17,6 +17,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
isGroupDecendent: true,
+ useDefaultState: true,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
projectSelect();
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index 13c5c350c24..71c67ac74ed 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -14,6 +14,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
isGroupDecendent: true,
+ useDefaultState: true,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
projectSelect();
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 d2684b6af59..83b38b0f1a5 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -2,7 +2,7 @@ import { debounce } from 'lodash';
import InputValidator from '~/validators/input_validator';
import fetchGroupPathAvailability from './fetch_group_path_availability';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
const debounceTimeoutDuration = 1000;
diff --git a/app/assets/javascripts/pages/groups/packages/index/index.js b/app/assets/javascripts/pages/groups/packages/index/index.js
new file mode 100644
index 00000000000..4836900aa28
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/packages/index/index.js
@@ -0,0 +1,7 @@
+import initPackageList from '~/packages/list/packages_list_app_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ if (document.getElementById('js-vue-packages-list')) {
+ initPackageList();
+ }
+});
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 23283f46a5d..add483843df 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,7 +2,7 @@ import initSettingsPanels from '~/settings_panels';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import initVariableList from '~/ci_variable_list';
import initFilteredSearch from '~/pages/search/init_filtered_search';
-import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys';
+import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
document.addEventListener('DOMContentLoaded', () => {
@@ -11,8 +11,9 @@ document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ADMIN_RUNNERS,
- filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys,
+ filteredSearchTokenKeys: GroupRunnersFilteredSearchTokenKeys,
anchor: FILTERED_SEARCH.GROUP_RUNNERS_ANCHOR,
+ useDefaultState: false,
});
if (gon.features.newVariablesUi) {
diff --git a/app/assets/javascripts/pages/import/bitbucket/status/index.js b/app/assets/javascripts/pages/import/bitbucket/status/index.js
index 52b5adb79d1..2a5432ce09d 100644
--- a/app/assets/javascripts/pages/import/bitbucket/status/index.js
+++ b/app/assets/javascripts/pages/import/bitbucket/status/index.js
@@ -7,13 +7,13 @@ document.addEventListener('DOMContentLoaded', () => {
if (!mountElement) return undefined;
const store = initStoreFromElement(mountElement);
- const props = initPropsFromElement(mountElement);
+ const attrs = initPropsFromElement(mountElement);
return new Vue({
el: mountElement,
store,
render(createElement) {
- return createElement(BitbucketStatusTable, { props });
+ return createElement(BitbucketStatusTable, { attrs });
},
});
});
diff --git a/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue b/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue
index e01c7b80e1a..35ae9d8419f 100644
--- a/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue
+++ b/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue
@@ -7,11 +7,8 @@ export default {
BitbucketStatusTable,
GlButton,
},
+ inheritAttrs: false,
props: {
- providerTitle: {
- type: String,
- required: true,
- },
reconfigurePath: {
type: String,
required: true,
@@ -20,7 +17,7 @@ export default {
};
</script>
<template>
- <bitbucket-status-table :provider-title="providerTitle">
+ <bitbucket-status-table v-bind="$attrs">
<template #actions>
<gl-button variant="info" class="gl-ml-3" data-method="post" :href="reconfigurePath">{{
__('Reconfigure')
diff --git a/app/assets/javascripts/pages/import/bitbucket_server/status/index.js b/app/assets/javascripts/pages/import/bitbucket_server/status/index.js
index 88455c9b7b9..a44fc4e6b29 100644
--- a/app/assets/javascripts/pages/import/bitbucket_server/status/index.js
+++ b/app/assets/javascripts/pages/import/bitbucket_server/status/index.js
@@ -7,14 +7,16 @@ document.addEventListener('DOMContentLoaded', () => {
if (!mountElement) return undefined;
const store = initStoreFromElement(mountElement);
- const props = initPropsFromElement(mountElement);
+ const attrs = initPropsFromElement(mountElement);
const { reconfigurePath } = mountElement.dataset;
return new Vue({
el: mountElement,
store,
render(createElement) {
- return createElement(BitbucketServerStatusTable, { props: { ...props, reconfigurePath } });
+ return createElement(BitbucketServerStatusTable, {
+ attrs: { ...attrs, reconfigurePath },
+ });
},
});
});
diff --git a/app/assets/javascripts/pages/import/manifest/status/index.js b/app/assets/javascripts/pages/import/manifest/status/index.js
new file mode 100644
index 00000000000..dcd84f0faf9
--- /dev/null
+++ b/app/assets/javascripts/pages/import/manifest/status/index.js
@@ -0,0 +1,7 @@
+import mountImportProjectsTable from '~/import_projects';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const mountElement = document.getElementById('import-projects-mount-element');
+
+ mountImportProjectsTable(mountElement);
+});
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index 58dba41277d..5be8e6697a2 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -1,7 +1,7 @@
<script>
import axios from '~/lib/utils/axios_utils';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { n__, s__, sprintf } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index e18732d0fd5..0dc54b612ba 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -1,6 +1,6 @@
<script>
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index 74ab1bc13a9..60510eac384 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import emojiRegex from 'emoji-regex';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import EmojiMenu from './emoji_menu';
import { __ } from '~/locale';
import * as Emoji from '~/emoji';
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index e5e4670a5d7..cb7198e9789 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -54,13 +54,9 @@ document.addEventListener('DOMContentLoaded', () => {
new Vue({
el: successPipelineEl,
render(createElement) {
- const { commitCookie, goToPipelinesPath, humanAccess } = this.$el.dataset;
-
return createElement(PipelineTourSuccessModal, {
props: {
- goToPipelinesPath,
- commitCookie,
- humanAccess,
+ ...successPipelineEl.dataset,
},
});
},
diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js
index d20e2c19583..a05ea8ae845 100644
--- a/app/assets/javascripts/pages/projects/clusters/show/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/show/index.js
@@ -1,9 +1,11 @@
import ClustersBundle from '~/clusters/clusters_bundle';
import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
import initClusterHealth from './cluster_health';
+import initIntegrationForm from '~/clusters/forms/show';
document.addEventListener('DOMContentLoaded', () => {
new ClustersBundle(); // eslint-disable-line no-new
initGkeNamespace();
initClusterHealth();
+ initIntegrationForm();
});
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 0eb6f231839..a245af72d93 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -7,23 +7,47 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown';
-import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
import '~/sourcegraph/load';
+import { handleLocationHash } from '~/lib/utils/common_utils';
+import axios from '~/lib/utils/axios_utils';
+import syntaxHighlight from '~/syntax_highlight';
+import flash from '~/flash';
+import { __ } from '~/locale';
document.addEventListener('DOMContentLoaded', () => {
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
- new Diff();
- new ZenMode();
- new ShortcutsNavigation();
- new MiniPipelineGraph({
- container: '.js-commit-pipeline-graph',
- }).bindEvents();
- initNotes();
- initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
- // eslint-disable-next-line no-jquery/no-load
- $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
- fetchCommitMergeRequests();
- initDiffNotes();
+ const filesContainer = $('.js-diffs-batch');
+ const initAfterPageLoad = () => {
+ new Diff();
+ new ZenMode();
+ new ShortcutsNavigation();
+ new MiniPipelineGraph({
+ container: '.js-commit-pipeline-graph',
+ }).bindEvents();
+ initNotes();
+ initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
+ // eslint-disable-next-line no-jquery/no-load
+ $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
+ fetchCommitMergeRequests();
+ };
+
+ if (filesContainer.length) {
+ const batchPath = filesContainer.data('diffFilesPath');
+
+ axios
+ .get(batchPath)
+ .then(({ data }) => {
+ filesContainer.html($(data.html));
+ syntaxHighlight(filesContainer);
+ handleLocationHash();
+ initAfterPageLoad();
+ })
+ .catch(() => {
+ flash(__('An error occurred while retrieving diff files'));
+ });
+ } else {
+ initAfterPageLoad();
+ }
});
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index e65c18c07a9..7eeb0c852e5 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -7,7 +7,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
import initProjectLoadingSpinner from '../shared/save_project_loader';
import initProjectPermissionsSettings from '../shared/permissions';
-import initProjectRemoveModal from '~/projects/project_remove_modal';
+import initProjectDeleteButton from '~/projects/project_delete_button';
import UserCallout from '~/user_callout';
import initServiceDesk from '~/projects/settings_service_desk';
@@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFilePickers();
initConfirmDangerModal();
initSettingsPanels();
- initProjectRemoveModal();
+ initProjectDeleteButton();
mountBadgeSettings(PROJECT_BADGE);
new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
index 77753521342..11ece478d36 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue
@@ -2,7 +2,7 @@
import { GlTabs, GlTab, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import ForkGroupsListItem from './fork_groups_list_item.vue';
export default {
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
index 792c2f3db34..b4816fa2cb3 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
@@ -35,7 +35,7 @@ export default {
},
},
data() {
- return { namespaces: null };
+ return { namespaces: null, isForking: false };
},
computed: {
@@ -67,6 +67,13 @@ export default {
},
},
+ methods: {
+ fork() {
+ this.isForking = true;
+ this.$refs.form.submit();
+ },
+ },
+
i18n: {
hasReachedProjectLimitMessage: __('You have reached your project limit'),
insufficientPermissionsMessage: __(
@@ -124,14 +131,17 @@ export default {
>
<template v-else>
<div ref="selectButtonWrapper">
- <form method="POST" :action="group.fork_path">
+ <form ref="form" method="POST" :action="group.fork_path">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<gl-button
type="submit"
- class="gl-h-7 gl-text-decoration-none!"
+ class="gl-h-7"
:data-qa-name="group.full_name"
+ category="secondary"
variant="success"
:disabled="isSelectButtonDisabled"
+ :loading="isForking"
+ @click="fork"
>{{ __('Select') }}</gl-button
>
</form>
diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js
index d80e27e9156..79485859738 100644
--- a/app/assets/javascripts/pages/projects/forks/new/index.js
+++ b/app/assets/javascripts/pages/projects/forks/new/index.js
@@ -1,3 +1,23 @@
-import ProjectFork from '~/project_fork';
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import ForkGroupsList from './components/fork_groups_list.vue';
-document.addEventListener('DOMContentLoaded', () => new ProjectFork());
+document.addEventListener('DOMContentLoaded', () => {
+ const mountElement = document.getElementById('fork-groups-mount-element');
+
+ const { endpoint, canCreateProject } = mountElement.dataset;
+
+ const hasReachedProjectLimit = !parseBoolean(canCreateProject);
+
+ return new Vue({
+ el: mountElement,
+ render(h) {
+ return h(ForkGroupsList, {
+ props: {
+ endpoint,
+ hasReachedProjectLimit,
+ },
+ });
+ },
+ });
+});
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 03504fba1ae..09b440d1413 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import { __ } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import { __ } from '~/locale';
import CodeCoverage from '../components/code_coverage.vue';
import SeriesDataMixin from './series_data_mixin';
diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
index 39d6df33a85..5d59880d497 100644
--- a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
+++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
@@ -1,9 +1,15 @@
<script>
-import { GlAlert, GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
+ GlIcon,
+ GlSprintf,
+} from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
-import axios from '~/lib/utils/axios_utils';
import { get } from 'lodash';
+import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -11,8 +17,8 @@ export default {
components: {
GlAlert,
GlAreaChart,
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
GlIcon,
GlSprintf,
},
@@ -134,8 +140,8 @@ export default {
{{ __('It seems that there is currently no available data for code coverage') }}
</span>
</gl-alert>
- <gl-dropdown v-if="canShowData" :text="selectedDailyCoverageName">
- <gl-dropdown-item
+ <gl-deprecated-dropdown v-if="canShowData" :text="selectedDailyCoverageName">
+ <gl-deprecated-dropdown-item
v-for="({ group_name }, index) in dailyCoverageData"
:key="index"
:value="group_name"
@@ -151,8 +157,8 @@ export default {
{{ group_name }}
</span>
</div>
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
</div>
<gl-area-chart
v-if="!isLoading"
diff --git a/app/assets/javascripts/pages/projects/incidents/index/index.js b/app/assets/javascripts/pages/projects/incidents/index/index.js
new file mode 100644
index 00000000000..c37ae862a85
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/incidents/index/index.js
@@ -0,0 +1,5 @@
+import IncidentsList from '~/incidents/list';
+
+document.addEventListener('DOMContentLoaded', () => {
+ IncidentsList();
+});
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index a66b665d152..1711d122080 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -17,6 +17,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ useDefaultState: true,
});
new IssuableIndex(ISSUABLE_INDEX.ISSUE);
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js
index 72003b61c8a..fc0922d9112 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js
@@ -9,6 +9,7 @@ export default class FilteredSearchServiceDesk extends FilteredSearchManager {
super({
page: 'service_desk',
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ useDefaultState: true,
});
this.supportBotData = supportBotData;
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
index 56054f5fc80..9304d9b6832 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,11 +1,17 @@
import FilteredSearchServiceDesk from './filtered_search';
+import initIssuablesList from '~/issuables_list';
document.addEventListener('DOMContentLoaded', () => {
const supportBotData = JSON.parse(
document.querySelector('.js-service-desk-issues').dataset.supportBot,
);
- const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
+ if (document.querySelector('.filtered-search')) {
+ const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
+ filteredSearchManager.setup();
+ }
- filteredSearchManager.setup();
+ if (gon.features?.vueIssuablesList) {
+ initIssuablesList();
+ }
});
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 32f77465347..5ac6c17e09d 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -3,24 +3,26 @@ import Issue from '~/issue';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
-import initIssueableApp, { issuableHeaderWarnings } from '~/issue_show';
+import { store } from '~/notes/stores';
+import initIssueableApp from '~/issue_show';
+import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
import initRelatedMergeRequestsApp from '~/related_merge_requests';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
export default function() {
initIssueableApp();
+ initIssuableHeaderWarning(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
- issuableHeaderWarnings();
- import(/* webpackChunkName: 'design_management' */ '~/design_management')
+ // This will be removed when we remove the `design_management_moved` feature flag
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/223197
+ import(/* webpackChunkName: 'design_management' */ '~/design_management_legacy')
.then(module => module.default())
.catch(() => {});
- // This will be removed when we remove the `design_management_moved` feature flag
- // See https://gitlab.com/gitlab-org/gitlab/-/issues/223197
- import(/* webpackChunkName: 'design_management' */ '~/design_management_new')
+ import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then(module => module.default())
.catch(() => {});
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 08078fa6b62..f58e4909a08 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -1,7 +1,7 @@
<script>
-import { escape } from 'lodash';
+import { GlSprintf } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -10,6 +10,7 @@ import eventHub from '../event_hub';
export default {
components: {
GlModal: DeprecatedModal2,
+ GlSprintf,
},
props: {
url: {
@@ -45,20 +46,6 @@ export default {
},
);
},
- title() {
- const label = `<span
- class="label color-label"
- style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
- >${escape(this.labelTitle)}</span>`;
-
- return sprintf(
- s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'),
- {
- labelTitle: label,
- },
- false,
- );
- },
},
methods: {
onSubmit() {
@@ -90,7 +77,27 @@ export default {
footer-primary-button-variant="warning"
@submit="onSubmit"
>
- <div slot="title" class="modal-title-with-label" v-html="title"></div>
+ <div slot="title" class="modal-title-with-label">
+ <gl-sprintf
+ :message="
+ s__(
+ 'Labels|%{spanStart}Promote label%{spanEnd} %{labelTitle} %{spanStart}to Group Label?%{spanEnd}',
+ )
+ "
+ >
+ <template #labelTitle>
+ <span
+ class="label color-label"
+ :style="`background-color: ${labelColor}; color: ${labelTextColor};`"
+ >
+ {{ labelTitle }}
+ </span>
+ </template>
+ <template #span="{ content }"
+ ><span>{{ content }}</span></template
+ >
+ </gl-sprintf>
+ </div>
{{ text }}
</gl-modal>
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 8f93cbb2a42..ce0b5c80927 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -13,6 +13,7 @@ document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ useDefaultState: true,
});
new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index c4cc667710a..25abb80cfae 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -6,7 +6,6 @@ import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
import initSourcegraph from '~/sourcegraph';
-import initPopover from '~/mr_tabs_popover';
export default function() {
new ZenMode(); // eslint-disable-line no-new
@@ -20,10 +19,4 @@ export default function() {
handleLocationHash();
howToMerge();
initSourcegraph();
-
- const tabHighlightEl = document.querySelector('.js-tabs-feature-highlight');
-
- if (tabHighlightEl) {
- initPopover(tabHighlightEl);
- }
}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index 4708970efef..29ebf656fe1 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -2,6 +2,8 @@ import initMrNotes from '~/mr_notes';
import { initReviewBar } from '~/batch_comments';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initShow from '../init_merge_request_show';
+import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
+import store from '~/mr_notes/stores';
document.addEventListener('DOMContentLoaded', () => {
initShow();
@@ -10,4 +12,5 @@ document.addEventListener('DOMContentLoaded', () => {
}
initMrNotes();
initReviewBar();
+ initIssuableHeaderWarning(store);
});
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index e17059dd55a..637ed28a758 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -1,7 +1,7 @@
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import Tracking from '~/tracking';
document.addEventListener('DOMContentLoaded', () => {
diff --git a/app/assets/javascripts/pages/projects/packages/packages/index/index.js b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
new file mode 100644
index 00000000000..4836900aa28
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
@@ -0,0 +1,7 @@
+import initPackageList from '~/packages/list/packages_list_app_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ if (document.getElementById('js-vue-packages-list')) {
+ initPackageList();
+ }
+});
diff --git a/app/assets/javascripts/pages/projects/packages/packages/show/index.js b/app/assets/javascripts/pages/projects/packages/packages/show/index.js
new file mode 100644
index 00000000000..1fde4ddfc1d
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/packages/packages/show/index.js
@@ -0,0 +1,3 @@
+import initPackageDetail from '~/packages/details/';
+
+document.addEventListener('DOMContentLoaded', initPackageDetail);
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
index b0b077a5e4c..d5563143f0c 100644
--- a/app/assets/javascripts/pages/projects/pipelines/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -1,12 +1,19 @@
import $ from 'jquery';
import NewBranchForm from '~/new_branch_form';
import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
+import initNewPipeline from '~/pipeline_new/index';
document.addEventListener('DOMContentLoaded', () => {
- new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
+ const el = document.getElementById('js-new-pipeline');
- setupNativeFormVariableList({
- container: $('.js-ci-variable-list-section'),
- formField: 'variables_attributes',
- });
+ if (el) {
+ initNewPipeline();
+ } else {
+ new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
+
+ setupNativeFormVariableList({
+ container: $('.js-ci-variable-list-section'),
+ formField: 'variables_attributes',
+ });
+ }
});
diff --git a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js b/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
new file mode 100644
index 00000000000..0539d318471
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
@@ -0,0 +1,3 @@
+import initActivityCharts from '~/analytics/product_analytics/activity_charts_bundle';
+
+document.addEventListener('DOMContentLoaded', () => initActivityCharts());
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 739ae1cea16..bb285635425 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -6,7 +6,7 @@ import { __ } from '~/locale';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { serializeForm } from '~/lib/utils/forms';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import projectSelect from '../../project_select';
export default class Project {
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 e08d0407245..ab2a7c099c4 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
@@ -3,6 +3,7 @@ import SecretValues from '~/behaviors/secret_values';
import AjaxVariableList from '~/ci_variable_list/ajax_variable_list';
import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
import initVariableList from '~/ci_variable_list';
+import initDeployFreeze from '~/deploy_freeze';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
@@ -40,4 +41,5 @@ document.addEventListener('DOMContentLoaded', () => {
});
registrySettingsApp();
+ initDeployFreeze();
});
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js
index 3e02893f24c..eff45bad603 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/form.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/form.js
@@ -14,7 +14,7 @@ export default () => {
new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
- new ProtectedBranchCreate();
+ new ProtectedBranchCreate({ hasLicense: false });
new ProtectedBranchEditList();
new DueDateSelectors();
fileUpload('.js-choose-file', '.js-object-map-input');
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js b/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
index fcbd81416f2..f69ca6e27b3 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/mixins/settings_pannel_mixin.js
@@ -13,7 +13,6 @@ export default {
if (value === 0) {
this.containerRegistryEnabled = false;
- this.lfsEnabled = false;
}
} else if (oldValue === 0) {
this.mergeRequestsAccessLevel = value;
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index c65cc3e4c57..fd522b975a6 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,3 +1,4 @@
+import initTree from 'ee_else_ce/repository';
import initBlob from '~/blob_edit/blob_bundle';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import NotificationsForm from '~/notifications_form';
@@ -9,7 +10,6 @@ import leaveByUrl from '~/namespaces/leave_by_url';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
import { showLearnGitLabProjectPopover } from '~/onboarding_issues';
-import initTree from 'ee_else_ce/repository';
document.addEventListener('DOMContentLoaded', () => {
initReadMore();
diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js
index 78a4ea23f1a..b19abda2821 100644
--- a/app/assets/javascripts/pages/projects/tree/show/index.js
+++ b/app/assets/javascripts/pages/projects/tree/show/index.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
+import initTree from 'ee_else_ce/repository';
import initBlob from '~/blob_edit/blob_bundle';
import ShortcutsNavigation from '../../../../behaviors/shortcuts/shortcuts_navigation';
import NewCommitForm from '../../../../new_commit_form';
-import initTree from 'ee_else_ce/repository';
document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index 4b4b0555bb2..a33d11f3613 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -1,20 +1,9 @@
import LengthValidator from '~/pages/sessions/new/length_validator';
import UsernameValidator from '~/pages/sessions/new/username_validator';
import NoEmojiValidator from '~/emoji/no_emoji_validator';
-import Tracking from '~/tracking';
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
new LengthValidator(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
});
-
-document.addEventListener('SnowplowInitialized', () => {
- if (gon.tracking_data) {
- const { category, action } = gon.tracking_data;
-
- if (category && action) {
- Tracking.event(category, action);
- }
- }
-});
diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js
index b331a2bee6a..eaed3246d06 100644
--- a/app/assets/javascripts/pages/search/init_filtered_search.js
+++ b/app/assets/javascripts/pages/search/init_filtered_search.js
@@ -6,6 +6,7 @@ export default ({
isGroup,
isGroupAncestor,
isGroupDecendent,
+ useDefaultState,
stateFiltersSelector,
anchor,
}) => {
@@ -16,6 +17,7 @@ export default ({
isGroup,
isGroupAncestor,
isGroupDecendent,
+ useDefaultState,
filteredSearchTokenKeys,
stateFiltersSelector,
anchor,
diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js
index 4050f2f13f1..cc2128490ec 100644
--- a/app/assets/javascripts/pages/search/show/search.js
+++ b/app/assets/javascripts/pages/search/show/search.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import '~/gl_dropdown';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import Api from '~/api';
import { __ } from '~/locale';
import Project from '~/pages/projects/project';
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 66ee2d9303f..55bc93a2b13 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -5,7 +5,6 @@ import NoEmojiValidator from '../../../emoji/no_emoji_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
-import Tracking from '~/tracking';
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
@@ -21,16 +20,3 @@ document.addEventListener('DOMContentLoaded', () => {
// redirected to sign-in after attempting to access a protected URL that included a fragment.
preserveUrlFragment(window.location.hash);
});
-
-export default function trackData() {
- if (gon.tracking_data) {
- const tab = document.querySelector(".new-session-tabs a[href='#register-pane']");
- const { category, action, ...data } = gon.tracking_data;
-
- tab.addEventListener('click', () => {
- Tracking.event(category, action, data);
- });
- }
-}
-
-trackData();
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index ecb5e677290..62f6e3fb84f 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -2,7 +2,7 @@ import { debounce } from 'lodash';
import InputValidator from '~/validators/input_validator';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
const debounceTimeoutDuration = 1000;
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 70e9333456d..eb0a5efe75c 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -5,7 +5,7 @@ import { select } from 'd3-selection';
import dateFormat from 'dateformat';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { n__, s__, __ } from '~/locale';
const d3 = { select, scaleLinear, scaleThreshold };
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index dafd800099c..9d66c784750 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -5,7 +5,7 @@ import Activities from '~/activities';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
import AjaxCache from '~/lib/utils/ajax_cache';
import { __ } from '~/locale';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import ActivityCalendar from './activity_calendar';
import UserOverviewBlock from './user_overview_block';
diff --git a/app/assets/javascripts/performance_constants.js b/app/assets/javascripts/performance_constants.js
new file mode 100644
index 00000000000..1a53b925aa4
--- /dev/null
+++ b/app/assets/javascripts/performance_constants.js
@@ -0,0 +1,12 @@
+//
+// SNIPPET namespace
+//
+
+// marks
+export const SNIPPET_MARK_VIEW_APP_START = 'snippet-view-app-start';
+export const SNIPPET_MARK_EDIT_APP_START = 'snippet-edit-app-start';
+export const SNIPPET_MARK_BLOBS_CONTENT = 'snippet-blobs-content-finished';
+
+// Measures
+export const SNIPPET_MEASURE_BLOBS_CONTENT = 'snippet-blobs-content';
+export const SNIPPET_MEASURE_BLOBS_CONTENT_WITHIN_APP = 'snippet-blobs-content-within-app';
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index b8a1397d8f6..eded64127b6 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -1,7 +1,7 @@
import { parseBoolean } from './lib/utils/common_utils';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
-import Flash from './flash';
+import { deprecatedCreateFlash as Flash } from './flash';
const DEFERRED_LINK_CLASS = 'deferred-link';
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index f4fe605f0a2..ef4d5338046 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -5,7 +5,6 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-users-over-license-callout',
'.js-admin-licensed-user-count-threshold',
'.js-buy-pipeline-minutes-notification-callout',
- '.js-alerts-moved-alert',
'.js-token-expiry-callout',
];
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
new file mode 100644
index 00000000000..e079603a5d4
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -0,0 +1,247 @@
+<script>
+import Vue from 'vue';
+import { uniqueId } from 'lodash';
+import {
+ GlAlert,
+ GlButton,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlFormSelect,
+ GlLink,
+ GlNewDropdown,
+ GlNewDropdownItem,
+ GlSearchBoxByType,
+ GlSprintf,
+} from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import Api from '~/api';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
+
+export default {
+ typeOptions: [
+ { value: VARIABLE_TYPE, text: __('Variable') },
+ { value: FILE_TYPE, text: __('File') },
+ ],
+ variablesDescription: s__(
+ 'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.',
+ ),
+ formElementClasses: 'gl-mr-3 gl-mb-3 table-section section-15',
+ errorTitle: __('The form contains the following error:'),
+ components: {
+ GlAlert,
+ GlButton,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlFormSelect,
+ GlLink,
+ GlNewDropdown,
+ GlNewDropdownItem,
+ GlSearchBoxByType,
+ GlSprintf,
+ },
+ props: {
+ pipelinesPath: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: true,
+ },
+ refs: {
+ type: Array,
+ required: true,
+ },
+ settingsLink: {
+ type: String,
+ required: true,
+ },
+ fileParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ refParam: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ variableParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ refValue: this.refParam,
+ variables: {},
+ error: false,
+ };
+ },
+ computed: {
+ filteredRefs() {
+ const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
+ return this.refs.filter(ref => ref.toLowerCase().includes(lowerCasedSearchTerm));
+ },
+ variablesLength() {
+ return Object.keys(this.variables).length;
+ },
+ },
+ created() {
+ if (this.variableParams) {
+ this.setVariableParams(VARIABLE_TYPE, this.variableParams);
+ }
+
+ if (this.fileParams) {
+ this.setVariableParams(FILE_TYPE, this.fileParams);
+ }
+
+ this.addEmptyVariable();
+ },
+ methods: {
+ addEmptyVariable() {
+ this.variables[uniqueId('var')] = {
+ variable_type: VARIABLE_TYPE,
+ key: '',
+ value: '',
+ };
+ },
+ setVariableParams(type, paramsObj) {
+ Object.entries(paramsObj).forEach(([key, value]) => {
+ this.variables[uniqueId('var')] = {
+ key,
+ value,
+ variable_type: type,
+ };
+ });
+ },
+ setRefSelected(ref) {
+ this.refValue = ref;
+ },
+ isSelected(ref) {
+ return ref === this.refValue;
+ },
+ insertNewVariable() {
+ Vue.set(this.variables, uniqueId('var'), {
+ variable_type: VARIABLE_TYPE,
+ key: '',
+ value: '',
+ });
+ },
+ removeVariable(key) {
+ Vue.delete(this.variables, key);
+ },
+
+ canRemove(index) {
+ return index < this.variablesLength - 1;
+ },
+ createPipeline() {
+ const filteredVariables = Object.values(this.variables).filter(
+ ({ key, value }) => key !== '' && value !== '',
+ );
+
+ return Api.createPipeline(this.projectId, {
+ ref: this.refValue,
+ variables: filteredVariables,
+ })
+ .then(({ data }) => redirectTo(data.web_url))
+ .catch(err => {
+ this.error = err.response.data.message.base;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form @submit.prevent="createPipeline">
+ <gl-alert
+ v-if="error"
+ :title="$options.errorTitle"
+ :dismissible="false"
+ variant="danger"
+ class="gl-mb-4"
+ >{{ error }}</gl-alert
+ >
+ <gl-form-group :label="s__('Pipeline|Run for')">
+ <gl-new-dropdown :text="refValue" block>
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ :placeholder="__('Search branches and tags')"
+ class="gl-p-2"
+ />
+ <gl-new-dropdown-item
+ v-for="(ref, index) in filteredRefs"
+ :key="index"
+ class="gl-font-monospace"
+ is-check-item
+ :is-checked="isSelected(ref)"
+ @click="setRefSelected(ref)"
+ >
+ {{ ref }}
+ </gl-new-dropdown-item>
+ </gl-new-dropdown>
+
+ <template #description>
+ <div>
+ {{ s__('Pipeline|Existing branch name or tag') }}
+ </div></template
+ >
+ </gl-form-group>
+
+ <gl-form-group :label="s__('Pipeline|Variables')">
+ <div
+ v-for="(value, key, index) in variables"
+ :key="key"
+ class="gl-display-flex gl-align-items-center gl-mb-4 gl-pb-2 gl-border-b-solid gl-border-gray-200 gl-border-b-1 gl-flex-direction-column gl-md-flex-direction-row"
+ data-testid="ci-variable-row"
+ >
+ <gl-form-select
+ v-model="variables[key].variable_type"
+ :class="$options.formElementClasses"
+ :options="$options.typeOptions"
+ />
+ <gl-form-input
+ v-model="variables[key].key"
+ :placeholder="s__('CiVariables|Input variable key')"
+ :class="$options.formElementClasses"
+ data-testid="pipeline-form-ci-variable-key"
+ @change.once="insertNewVariable()"
+ />
+ <gl-form-input
+ v-model="variables[key].value"
+ :placeholder="s__('CiVariables|Input variable value')"
+ class="gl-mr-5 gl-mb-3 table-section section-15"
+ />
+ <gl-button
+ v-if="canRemove(index)"
+ icon="issue-close"
+ class="gl-mb-3"
+ data-testid="remove-ci-variable-row"
+ @click="removeVariable(key)"
+ />
+ </div>
+
+ <template #description
+ ><gl-sprintf :message="$options.variablesDescription">
+ <template #link="{ content }">
+ <gl-link :href="settingsLink">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf></template
+ >
+ </gl-form-group>
+ <div
+ class="gl-border-t-solid gl-border-gray-100 gl-border-t-1 gl-p-5 gl-bg-gray-10 gl-display-flex gl-justify-content-space-between"
+ >
+ <gl-button type="submit" category="primary" variant="success">{{
+ s__('Pipeline|Run Pipeline')
+ }}</gl-button>
+ <gl-button :href="pipelinesPath">{{ __('Cancel') }}</gl-button>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/pipeline_new/constants.js b/app/assets/javascripts/pipeline_new/constants.js
new file mode 100644
index 00000000000..b4ab1143f60
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/constants.js
@@ -0,0 +1,2 @@
+export const VARIABLE_TYPE = 'env_var';
+export const FILE_TYPE = 'file';
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js
new file mode 100644
index 00000000000..1c4812c2e0e
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/index.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import PipelineNewForm from './components/pipeline_new_form.vue';
+
+export default () => {
+ const el = document.getElementById('js-new-pipeline');
+ const {
+ projectId,
+ pipelinesPath,
+ refParam,
+ varParam,
+ fileParam,
+ refNames,
+ settingsLink,
+ } = el?.dataset;
+
+ const variableParams = JSON.parse(varParam);
+ const fileParams = JSON.parse(fileParam);
+ const refs = JSON.parse(refNames);
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(PipelineNewForm, {
+ props: {
+ projectId,
+ pipelinesPath,
+ refParam,
+ variableParams,
+ fileParams,
+ refs,
+ settingsLink,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
index 85163a666e2..8487da3d621 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag.vue
@@ -1,8 +1,9 @@
<script>
-import { GlAlert, GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlButton, GlEmptyState, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
+import { fetchPolicies } from '~/lib/graphql';
+import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql';
import DagGraph from './dag_graph.vue';
import DagAnnotations from './dag_annotations.vue';
import {
@@ -23,35 +24,68 @@ export default {
DagAnnotations,
DagGraph,
GlAlert,
- GlLink,
GlSprintf,
GlEmptyState,
GlButton,
},
- props: {
- graphUrl: {
- type: String,
- required: false,
- default: '',
+ inject: {
+ dagDocPath: {
+ default: null,
},
emptySvgPath: {
- type: String,
- required: true,
default: '',
},
- dagDocPath: {
- type: String,
- required: true,
+ pipelineIid: {
+ default: '',
+ },
+ pipelineProjectPath: {
default: '',
},
},
+ apollo: {
+ graphData: {
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ query: getDagVisData,
+ variables() {
+ return {
+ projectPath: this.pipelineProjectPath,
+ iid: this.pipelineIid,
+ };
+ },
+ update(data) {
+ const {
+ stages: { nodes: stages },
+ } = data.project.pipeline;
+
+ const unwrappedGroups = stages
+ .map(({ name, groups: { nodes: groups } }) => {
+ return groups.map(group => {
+ return { category: name, ...group };
+ });
+ })
+ .flat(2);
+
+ const nodes = unwrappedGroups.map(group => {
+ const jobs = group.jobs.nodes.map(({ name, needs }) => {
+ return { name, needs: needs.nodes.map(need => need.name) };
+ });
+
+ return { ...group, jobs };
+ });
+
+ return nodes;
+ },
+ error() {
+ this.reportFailure(LOAD_FAILURE);
+ },
+ },
+ },
data() {
return {
annotationsMap: {},
failureType: null,
graphData: null,
showFailureAlert: false,
- showBetaInfo: true,
hasNoDependentJobs: false,
};
},
@@ -72,11 +106,6 @@ export default {
button: __('Learn more about job dependencies'),
},
computed: {
- betaMessage() {
- return __(
- 'This feature is currently in beta. We invite you to %{linkStart}give feedback%{linkEnd}.',
- );
- },
failure() {
switch (this.failureType) {
case LOAD_FAILURE:
@@ -97,32 +126,20 @@ export default {
default:
return {
text: this.$options.errorTexts[DEFAULT],
- vatiant: 'danger',
+ variant: 'danger',
};
}
},
+ processedData() {
+ return this.processGraphData(this.graphData);
+ },
shouldDisplayAnnotations() {
return !isEmpty(this.annotationsMap);
},
shouldDisplayGraph() {
- return Boolean(!this.showFailureAlert && this.graphData);
+ return Boolean(!this.showFailureAlert && !this.hasNoDependentJobs && this.graphData);
},
},
- mounted() {
- const { processGraphData, reportFailure } = this;
-
- if (!this.graphUrl) {
- reportFailure();
- return;
- }
-
- axios
- .get(this.graphUrl)
- .then(response => {
- processGraphData(response.data);
- })
- .catch(() => reportFailure(LOAD_FAILURE));
- },
methods: {
addAnnotationToMap({ uid, source, target }) {
this.$set(this.annotationsMap, uid, { source, target });
@@ -131,32 +148,29 @@ export default {
let parsed;
try {
- parsed = parseData(data.stages);
+ parsed = parseData(data);
} catch {
this.reportFailure(PARSE_FAILURE);
- return;
+ return {};
}
if (parsed.links.length === 1) {
this.reportFailure(UNSUPPORTED_DATA);
- return;
+ return {};
}
// If there are no links, we don't report failure
// as it simply means the user does not use job dependencies
if (parsed.links.length === 0) {
this.hasNoDependentJobs = true;
- return;
+ return {};
}
- this.graphData = parsed;
+ return parsed;
},
hideAlert() {
this.showFailureAlert = false;
},
- hideBetaInfo() {
- this.showBetaInfo = false;
- },
removeAnnotationFromMap({ uid }) {
this.$delete(this.annotationsMap, uid);
},
@@ -188,20 +202,11 @@ export default {
{{ failure.text }}
</gl-alert>
- <gl-alert v-if="showBetaInfo" @dismiss="hideBetaInfo">
- <gl-sprintf :message="betaMessage">
- <template #link="{ content }">
- <gl-link href="https://gitlab.com/gitlab-org/gitlab/-/issues/220368" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
<div class="gl-relative">
<dag-annotations v-if="shouldDisplayAnnotations" :annotations="annotationsMap" />
<dag-graph
v-if="shouldDisplayGraph"
- :graph-data="graphData"
+ :graph-data="processedData"
@onFailure="reportFailure"
@update-annotation="updateAnnotation"
/>
@@ -228,7 +233,7 @@ export default {
</p>
</div>
</template>
- <template #actions>
+ <template v-if="dagDocPath" #actions>
<gl-button :href="dagDocPath" target="__blank" variant="success">
{{ $options.emptyStateTexts.button }}
</gl-button>
diff --git a/app/assets/javascripts/pipelines/components/dag/parsing_utils.js b/app/assets/javascripts/pipelines/components/dag/parsing_utils.js
index 3234f80ee91..1ed415688f2 100644
--- a/app/assets/javascripts/pipelines/components/dag/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/dag/parsing_utils.js
@@ -5,14 +5,16 @@ import { uniqWith, isEqual } from 'lodash';
received from the endpoint into the format the d3 graph expects.
Input is of the form:
- [stages]
- stages: {name, groups}
- groups: [{ name, size, jobs }]
- name is a group name; in the case that the group has one job, it is
- also the job name
- size is the number of parallel jobs
- jobs: [{ name, needs}]
- job name is either the same as the group name or group x/y
+ [nodes]
+ nodes: [{category, name, jobs, size}]
+ category is the stage name
+ name is a group name; in the case that the group has one job, it is
+ also the job name
+ size is the number of parallel jobs
+ jobs: [{ name, needs}]
+ job name is either the same as the group name or group x/y
+ needs: [job-names]
+ needs is an array of job-name strings
Output is of the form:
{ nodes: [node], links: [link] }
@@ -20,30 +22,17 @@ import { uniqWith, isEqual } from 'lodash';
link: { source, target, value }, with source & target being node names
and value being a constant
- We create nodes, create links, and then dedupe the links, so that in the case where
+ We create nodes in the GraphQL update function, and then here we create the node dictionary,
+ then create links, and then dedupe the links, so that in the case where
job 4 depends on job 1 and job 2, and job 2 depends on job 1, we show only a single link
from job 1 to job 2 then another from job 2 to job 4.
- CREATE NODES
- stage.name -> node.category
- stage.group.name -> node.name (this is the group name if there are parallel jobs)
- stage.group.jobs -> node.jobs
- stage.group.size -> node.size
-
CREATE LINKS
- stages.groups.name -> target
- stages.groups.needs.each -> source (source is the name of the group, not the parallel job)
+ nodes.name -> target
+ nodes.name.needs.each -> source (source is the name of the group, not the parallel job)
10 -> value (constant)
*/
-export const createNodes = data => {
- return data.flatMap(({ groups, name }) => {
- return groups.map(group => {
- return { ...group, category: name };
- });
- });
-};
-
export const createNodeDict = nodes => {
return nodes.reduce((acc, node) => {
const newNode = {
@@ -62,13 +51,6 @@ export const createNodeDict = nodes => {
}, {});
};
-export const createNodesStructure = data => {
- const nodes = createNodes(data);
- const nodeDict = createNodeDict(nodes);
-
- return { nodes, nodeDict };
-};
-
export const makeLinksFromNodes = (nodes, nodeDict) => {
const constantLinkValue = 10; // all links are the same weight
return nodes
@@ -126,8 +108,8 @@ export const filterByAncestors = (links, nodeDict) =>
return !allAncestors.includes(source);
});
-export const parseData = data => {
- const { nodes, nodeDict } = createNodesStructure(data);
+export const parseData = nodes => {
+ const nodeDict = createNodeDict(nodes);
const allLinks = makeLinksFromNodes(nodes, nodeDict);
const filteredLinks = filterByAncestors(allLinks, nodeDict);
const links = uniqWith(filteredLinks, isEqual);
diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue
index c5e95036f4f..137455bcae1 100644
--- a/app/assets/javascripts/pipelines/components/graph/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue
@@ -1,9 +1,9 @@
<script>
-import { GlTooltipDirective, GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
/**
@@ -19,7 +19,7 @@ import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
},
directives: {
@@ -82,16 +82,16 @@ export default {
};
</script>
<template>
- <gl-deprecated-button
+ <gl-button
:id="`js-ci-action-${link}`"
v-gl-tooltip="{ boundary: 'viewport' }"
:title="tooltipText"
:class="cssClass"
:disabled="isDisabled"
- class="js-ci-action btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper d-flex align-items-center justify-content-center"
+ class="js-ci-action ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
@click="onClickAction"
>
<gl-loading-icon v-if="isLoading" class="js-action-icon-loading" />
<icon v-else :name="actionIcon" />
- </gl-deprecated-button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 6b890688a48..f5bf6a6ed34 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -170,7 +170,7 @@ export default {
v-for="(stage, index) in graph"
:key="stage.name"
:class="{
- 'has-upstream prepend-left-64': hasUpstream(index),
+ 'has-upstream gl-ml-11': hasUpstream(index),
'has-only-one-job': hasOnlyOneJob(stage),
'gl-mr-26': shouldAddRightMargin(index),
}"
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index 733553e02c0..f0a8f9f7ab7 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon, GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __, sprintf } from '~/locale';
@@ -9,8 +9,7 @@ export default {
},
components: {
CiStatus,
- GlLoadingIcon,
- GlDeprecatedButton,
+ GlButton,
},
props: {
pipeline: {
@@ -95,26 +94,21 @@ export default {
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
- <gl-deprecated-button
+ <gl-button
:id="buttonId"
v-gl-tooltip
:title="tooltipText"
- class="js-linked-pipeline-content linked-pipeline-content"
+ class="linked-pipeline-content"
data-qa-selector="linked_pipeline_button"
:class="`js-pipeline-expand-${pipeline.id}`"
+ :loading="pipeline.isLoading"
@click="onClickLinkedPipeline"
>
- <gl-loading-icon v-if="pipeline.isLoading" class="js-linked-pipeline-loading d-inline" />
- <ci-status
- v-else
- :status="pipelineStatus"
- css-classes="position-top-0"
- class="js-linked-pipeline-status"
- />
+ <ci-status v-if="!pipeline.isLoading" :status="pipelineStatus" css-classes="gl-top-0" />
<span class="str-truncated"> {{ downstreamTitle }} &#8226; #{{ pipeline.id }} </span>
<div class="gl-pt-2">
<span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span>
</div>
- </gl-deprecated-button>
+ </gl-button>
</li>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index c4dfd3382a2..d82885ff8de 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -27,7 +27,7 @@ export default {
computed: {
columnClass() {
const positionValues = {
- right: 'prepend-left-64',
+ right: 'gl-ml-11',
left: 'gl-mr-7',
};
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index dff642161db..c7b72be36ad 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -1,7 +1,6 @@
<script>
-import { GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
import ciHeader from '~/vue_shared/components/header_ci_component.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
import { __ } from '~/locale';
@@ -13,7 +12,7 @@ export default {
ciHeader,
GlLoadingIcon,
GlModal,
- LoadingButton,
+ GlButton,
},
directives: {
GlModal: GlModalDirective,
@@ -77,35 +76,43 @@ export default {
:user="pipeline.user"
item-name="Pipeline"
>
- <loading-button
+ <gl-button
v-if="pipeline.retry_path"
:loading="isRetrying"
:disabled="isRetrying"
- class="js-retry-button btn btn-inverted-secondary"
- container-class="d-inline"
- :label="__('Retry')"
+ data-testid="retryButton"
+ category="secondary"
+ variant="info"
@click="retryPipeline()"
- />
+ >
+ {{ __('Retry') }}
+ </gl-button>
- <loading-button
+ <gl-button
v-if="pipeline.cancel_path"
:loading="isCanceling"
:disabled="isCanceling"
- class="js-btn-cancel-pipeline btn btn-danger"
- container-class="d-inline"
- :label="__('Cancel running')"
+ data-testid="cancelPipeline"
+ class="gl-ml-3"
+ category="primary"
+ variant="danger"
@click="cancelPipeline()"
- />
+ >
+ {{ __('Cancel running') }}
+ </gl-button>
- <loading-button
+ <gl-button
v-if="pipeline.delete_path"
v-gl-modal="$options.DELETE_MODAL_ID"
:loading="isDeleting"
:disabled="isDeleting"
- class="js-btn-delete-pipeline btn btn-danger btn-inverted"
- container-class="d-inline"
- :label="__('Delete')"
- />
+ data-testid="deletePipeline"
+ class="gl-ml-3"
+ category="secondary"
+ variant="danger"
+ >
+ {{ __('Delete') }}
+ </gl-button>
</ci-header>
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3" />
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
index 74ada6a4d15..fe8e3bd2b78 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
@@ -1,10 +1,10 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
export default {
name: 'PipelinesEmptyState',
components: {
- GlDeprecatedButton,
+ GlButton,
},
props: {
helpPagePath: {
@@ -43,13 +43,14 @@ export default {
</p>
<div class="text-center">
- <gl-deprecated-button
+ <gl-button
:href="helpPagePath"
- variant="primary"
+ variant="info"
+ category="primary"
class="js-get-started-pipelines"
>
{{ s__('Pipelines|Get started with Pipelines') }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
index 2905b2ca26f..f0614298bd3 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
@@ -1,27 +1,15 @@
<script>
-import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-import { escape } from 'lodash';
+import { GlLink, GlPopover, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { SCHEDULE_ORIGIN } from '../../constants';
-import { __, sprintf } from '~/locale';
-import popover from '~/vue_shared/directives/popover';
-
-const popoverTitle = sprintf(
- escape(
- __(
- `This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}`,
- ),
- ),
- { strongStart: '<b>', strongEnd: '</b>' },
- false,
-);
export default {
components: {
GlLink,
+ GlPopover,
+ GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
- popover,
},
props: {
pipeline: {
@@ -44,23 +32,6 @@ export default {
isScheduled() {
return this.pipeline.source === SCHEDULE_ORIGIN;
},
- popoverOptions() {
- return {
- html: true,
- trigger: 'focus',
- placement: 'top',
- title: `<div class="autodevops-title">
- ${popoverTitle}
- </div>`,
- content: `<a
- class="autodevops-link"
- href="${this.autoDevopsHelpPath}"
- target="_blank"
- rel="noopener noreferrer nofollow">
- ${escape(__('Learn more about Auto DevOps'))}
- </a>`,
- };
- },
},
};
</script>
@@ -114,13 +85,42 @@ export default {
</span>
<gl-link
v-if="pipeline.flags.auto_devops"
- v-popover="popoverOptions"
+ :id="`pipeline-url-autodevops-${pipeline.id}`"
tabindex="0"
class="js-pipeline-url-autodevops badge badge-info autodevops-badge"
data-testid="pipeline-url-autodevops"
role="button"
>{{ __('Auto DevOps') }}</gl-link
>
+ <gl-popover
+ :target="`pipeline-url-autodevops-${pipeline.id}`"
+ triggers="focus"
+ placement="top"
+ >
+ <template #title>
+ <div class="autodevops-title">
+ <gl-sprintf
+ :message="
+ __(
+ 'This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}',
+ )
+ "
+ >
+ <template #strong="{content}">
+ <b>{{ content }}</b>
+ </template>
+ </gl-sprintf>
+ </div>
+ </template>
+ <gl-link
+ class="autodevops-link"
+ :href="autoDevopsHelpPath"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ >
+ {{ __('Learn more about Auto DevOps') }}
+ </gl-link>
+ </gl-popover>
<span
v-if="pipeline.flags.stuck"
class="js-pipeline-url-stuck badge badge-warning"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index 0c531650fd2..2dfc6485d85 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 { isEqual } from 'lodash';
import { __, s__ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import PipelinesService from '../../services/pipelines_service';
import pipelinesMixin from '../../mixins/pipelines';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
index 3009ca7a775..098efe68b83 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
@@ -1,7 +1,7 @@
<script>
import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { s__, __, sprintf } from '~/locale';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import Icon from '~/vue_shared/components/icon.vue';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
index 0505a8668d1..29345f33367 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
@@ -1,11 +1,11 @@
<script>
import { GlFilteredSearch } from '@gitlab/ui';
+import { map } from 'lodash';
import { __, s__ } from '~/locale';
import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
import PipelineStatusToken from './tokens/pipeline_status_token.vue';
import PipelineTagNameToken from './tokens/pipeline_tag_name_token.vue';
-import { map } from 'lodash';
export default {
userType: 'username',
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
index 99492bd8357..d992a4b7752 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
@@ -15,7 +15,7 @@
import $ from 'jquery';
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import eventHub from '../../event_hub';
import Icon from '~/vue_shared/components/icon.vue';
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 b6eff2931d3..60cb697f1af 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
@@ -1,9 +1,9 @@
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
+import { debounce } from 'lodash';
import Api from '~/api';
import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
-import createFlash from '~/flash';
-import { debounce } from 'lodash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
export default {
components: {
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 64de6d2a053..d6ba5fcca85 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
@@ -1,9 +1,9 @@
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
+import { debounce } from 'lodash';
import Api from '~/api';
import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
-import createFlash from '~/flash';
-import { debounce } from 'lodash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
export default {
components: {
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 b5aeb3fe9e0..dfa6d8c13a5 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
@@ -3,12 +3,12 @@ import {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
+ GlDeprecatedDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
-import Api from '~/api';
-import createFlash from '~/flash';
import { debounce } from 'lodash';
+import Api from '~/api';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
ANY_TRIGGER_AUTHOR,
FETCH_AUTHOR_ERROR_MESSAGE,
@@ -21,7 +21,7 @@ export default {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
+ GlDeprecatedDropdownDivider,
GlLoadingIcon,
},
props: {
@@ -94,7 +94,7 @@ export default {
<gl-filtered-search-suggestion :value="$options.anyTriggerAuthor">{{
$options.anyTriggerAuthor
}}</gl-filtered-search-suggestion>
- <gl-dropdown-divider />
+ <gl-deprecated-dropdown-divider />
<gl-loading-icon v-if="loading" />
<template v-else>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
index 8746784aa57..bc1d22e2976 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
@@ -14,7 +14,7 @@ export default {
TestSummaryTable,
},
computed: {
- ...mapState(['hasFullReport', 'isLoading', 'selectedSuiteIndex', 'testReports']),
+ ...mapState(['isLoading', 'selectedSuiteIndex', 'testReports']),
...mapGetters(['getSelectedSuite']),
showSuite() {
return this.selectedSuiteIndex !== null;
@@ -29,7 +29,7 @@ export default {
},
methods: {
...mapActions([
- 'fetchFullReport',
+ 'fetchTestSuite',
'fetchSummary',
'setSelectedSuiteIndex',
'removeSelectedSuiteIndex',
@@ -40,10 +40,8 @@ export default {
summaryTableRowClick(index) {
this.setSelectedSuiteIndex(index);
- // Fetch the full report when the user clicks to see more details
- if (!this.hasFullReport) {
- this.fetchFullReport();
- }
+ // Fetch the test suite when the user clicks to see more details
+ this.fetchTestSuite(index);
},
beforeEnterTransition() {
document.documentElement.style.overflowX = 'hidden';
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index d57b1466177..478073e44d1 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -1,8 +1,8 @@
<script>
import { mapGetters } from 'vuex';
+import { GlTooltipDirective, GlFriendlyWrap } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
-import { GlTooltipDirective } from '@gitlab/ui';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
@@ -10,6 +10,7 @@ export default {
components: {
Icon,
SmartVirtualList,
+ GlFriendlyWrap,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -29,6 +30,7 @@ export default {
},
maxShownRows: 30,
typicalRowHeight: 75,
+ wrapSymbols: ['::', '#', '.', '_', '-', '/', '\\'],
};
</script>
@@ -71,23 +73,19 @@ export default {
>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
- <div
- v-gl-tooltip
- :title="testCase.classname"
- class="table-mobile-content pr-md-1 text-truncate"
- >
- {{ testCase.classname }}
+ <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
+ <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" />
</div>
</div>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
- <div
- v-gl-tooltip
- :title="testCase.name"
- class="table-mobile-content pr-md-1 text-truncate"
- >
- {{ testCase.name }}
+ <div class="table-mobile-content pr-md-1 gl-overflow-wrap-break">
+ <gl-friendly-wrap
+ data-testid="caseName"
+ :symbols="$options.wrapSymbols"
+ :text="testCase.name"
+ />
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
index 6cfb795595d..e774fe06fbe 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
@@ -1,7 +1,7 @@
<script>
import { mapGetters } from 'vuex';
-import { s__ } from '~/locale';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
diff --git a/app/assets/javascripts/pipelines/event_hub.js b/app/assets/javascripts/pipelines/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/pipelines/event_hub.js
+++ b/app/assets/javascripts/pipelines/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
new file mode 100644
index 00000000000..c73b186739e
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
@@ -0,0 +1,27 @@
+query getDagVisData($projectPath: ID!, $iid: ID!) {
+ project(fullPath: $projectPath) {
+ pipeline(iid: $iid) {
+ stages {
+ nodes {
+ name
+ groups {
+ nodes {
+ name
+ size
+ jobs {
+ nodes {
+ name
+ needs {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
index f987c8f1dd4..886a8a78448 100644
--- a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
@@ -1,4 +1,4 @@
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 7710a96e5fb..e31545bba5c 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,7 +1,7 @@
import Visibility from 'visibilityjs';
import { GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import Poll from '~/lib/utils/poll';
import EmptyState from '../components/pipelines_list/empty_state.vue';
import SvgBlankState from '../components/pipelines_list/blank_state.vue';
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index f1102a9bddf..c57be7c75b0 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility';
import pipelineGraph from './components/graph/graph_component.vue';
-import Dag from './components/dag/dag.vue';
+import createDagApp from './pipeline_details_dag';
import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin';
import PipelinesMediator from './pipeline_details_mediator';
import pipelineHeader from './components/header_component.vue';
@@ -92,48 +92,15 @@ const createPipelineHeaderApp = mediator => {
});
};
-const createPipelinesTabs = testReportsStore => {
- const tabsElement = document.querySelector('.pipelines-tabs');
-
- if (tabsElement) {
- const fetchReportsAction = 'fetchFullReport';
- const isTestTabActive = Boolean(
- document.querySelector('.pipelines-tabs > li > a.test-tab.active'),
- );
-
- if (isTestTabActive) {
- testReportsStore.dispatch(fetchReportsAction);
- } else {
- const tabClickHandler = e => {
- if (e.target.className === 'test-tab') {
- testReportsStore.dispatch(fetchReportsAction);
- tabsElement.removeEventListener('click', tabClickHandler);
- }
- };
-
- tabsElement.addEventListener('click', tabClickHandler);
- }
- }
-};
-
const createTestDetails = () => {
- if (!window.gon?.features?.junitPipelineView) {
- return;
- }
-
const el = document.querySelector('#js-pipeline-tests-detail');
- const { fullReportEndpoint, summaryEndpoint, countEndpoint } = el?.dataset || {};
+ const { summaryEndpoint, suiteEndpoint } = el?.dataset || {};
const testReportsStore = createTestReportsStore({
- fullReportEndpoint,
- summaryEndpoint: summaryEndpoint || countEndpoint,
- useBuildSummaryReport: window.gon?.features?.buildReportSummary,
+ summaryEndpoint,
+ suiteEndpoint,
});
- if (!window.gon?.features?.buildReportSummary) {
- createPipelinesTabs(testReportsStore);
- }
-
// eslint-disable-next-line no-new
new Vue({
el,
@@ -147,32 +114,6 @@ const createTestDetails = () => {
});
};
-const createDagApp = () => {
- if (!window.gon?.features?.dagPipelineTab) {
- return;
- }
-
- const el = document.querySelector('#js-pipeline-dag-vue');
- const { pipelineDataPath, emptySvgPath, dagDocPath } = el?.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- Dag,
- },
- render(createElement) {
- return createElement('dag', {
- props: {
- graphUrl: pipelineDataPath,
- emptySvgPath,
- dagDocPath,
- },
- });
- },
- });
-};
-
export default () => {
const { dataset } = document.querySelector('.js-pipeline-details-vue');
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
diff --git a/app/assets/javascripts/pipelines/pipeline_details_dag.js b/app/assets/javascripts/pipelines/pipeline_details_dag.js
new file mode 100644
index 00000000000..dc03b457265
--- /dev/null
+++ b/app/assets/javascripts/pipelines/pipeline_details_dag.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import Dag from './components/dag/dag.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+const createDagApp = () => {
+ if (!window.gon?.features?.dagPipelineTab) {
+ return;
+ }
+
+ const el = document.querySelector('#js-pipeline-dag-vue');
+ const { pipelineProjectPath, pipelineIid, emptySvgPath, dagDocPath } = el?.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ Dag,
+ },
+ apolloProvider,
+ provide: {
+ pipelineProjectPath,
+ pipelineIid,
+ emptySvgPath,
+ dagDocPath,
+ },
+ render(createElement) {
+ return createElement('dag', {});
+ },
+ });
+};
+
+export default createDagApp;
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index f3387f00fc1..d487970aed7 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -1,6 +1,6 @@
import Visibility from 'visibilityjs';
import PipelineStore from './stores/pipeline_store';
-import Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import Poll from '../lib/utils/poll';
import { __ } from '../locale';
import PipelineService from './services/pipeline_service';
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
index ccacb9f7e97..f10bbeec77c 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -1,46 +1,46 @@
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
export const fetchSummary = ({ state, commit, dispatch }) => {
- // If we do this without the build_report_summary feature flag enabled
- // it causes a race condition for toggleLoading and ruins the loading
- // state in the application
- if (state.useBuildSummaryReport) {
- dispatch('toggleLoading');
- }
+ dispatch('toggleLoading');
return axios
.get(state.summaryEndpoint)
.then(({ data }) => {
commit(types.SET_SUMMARY, data);
-
- if (!state.useBuildSummaryReport) {
- // Set the tab counter badge to total_count
- // This is temporary until we can server-side render that count number
- // (see https://gitlab.com/gitlab-org/gitlab/-/issues/223134)
- document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count || 0;
- }
})
.catch(() => {
createFlash(s__('TestReports|There was an error fetching the summary.'));
})
.finally(() => {
- if (state.useBuildSummaryReport) {
- dispatch('toggleLoading');
- }
+ dispatch('toggleLoading');
});
};
-export const fetchFullReport = ({ state, commit, dispatch }) => {
+export const fetchTestSuite = ({ state, commit, dispatch }, index) => {
+ const { hasFullSuite } = state.testReports?.test_suites?.[index] || {};
+ // We don't need to fetch the suite if we have the information already
+ if (hasFullSuite) {
+ return Promise.resolve();
+ }
+
dispatch('toggleLoading');
+ const { name = '', build_ids = [] } = state.testReports?.test_suites?.[index] || {};
+ // Replacing `/:suite_name.json` with the name of the suite. Including the extra characters
+ // to ensure that we replace exactly the template part of the URL string
+ const endpoint = state.suiteEndpoint?.replace(
+ '/:suite_name.json',
+ `/${encodeURIComponent(name)}.json`,
+ );
+
return axios
- .get(state.fullReportEndpoint)
- .then(({ data }) => commit(types.SET_REPORTS, data))
+ .get(endpoint, { params: { build_ids } })
+ .then(({ data }) => commit(types.SET_SUITE, { suite: data, index }))
.catch(() => {
- createFlash(s__('TestReports|There was an error fetching the test reports.'));
+ createFlash(s__('TestReports|There was an error fetching the test suite.'));
})
.finally(() => {
dispatch('toggleLoading');
@@ -52,6 +52,3 @@ export const setSelectedSuiteIndex = ({ commit }, data) =>
export const removeSelectedSuiteIndex = ({ commit }) =>
commit(types.SET_SELECTED_SUITE_INDEX, null);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING);
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
index 877762b77c9..6c670806cc4 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
@@ -16,6 +16,3 @@ export const getSuiteTests = state => {
const { test_cases: testCases = [] } = getSelectedSuite(state);
return testCases.sort(sortTestCases).map(addIconStatus);
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
index 76405557b51..52345888cb0 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
@@ -1,4 +1,4 @@
-export const SET_REPORTS = 'SET_REPORTS';
export const SET_SELECTED_SUITE_INDEX = 'SET_SELECTED_SUITE_INDEX';
export const SET_SUMMARY = 'SET_SUMMARY';
+export const SET_SUITE = 'SET_SUITE';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
index 2531ab1e87c..3652a12a6ba 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
@@ -1,16 +1,41 @@
import * as types from './mutation_types';
export default {
- [types.SET_REPORTS](state, testReports) {
- Object.assign(state, { testReports, hasFullReport: true });
+ [types.SET_SUITE](state, { suite = {}, index = null }) {
+ state.testReports.test_suites[index] = { ...suite, hasFullSuite: true };
},
[types.SET_SELECTED_SUITE_INDEX](state, selectedSuiteIndex) {
Object.assign(state, { selectedSuiteIndex });
},
- [types.SET_SUMMARY](state, summary) {
- Object.assign(state, { testReports: { ...state.testReports, ...summary } });
+ [types.SET_SUMMARY](state, testReports) {
+ const { total } = testReports;
+ state.testReports = {
+ ...testReports,
+
+ /*
+ TLDR; this is a temporary mapping that will be updated once
+ test suites have the new data schema
+
+ The backend is in the middle of updating the data schema
+ to have a `total` object containing the total data values.
+ The test suites don't have the new schema, but the summary
+ does. Currently the `test_summary.vue` component takes both
+ the summary and a test suite depending on what is being viewed.
+ This is a temporary change to map the new schema to the old until
+ we can update the schema for the test suites also.
+ Since test suites is an array, it is easier to just map the summary
+ to the old schema instead of mapping every test suite to the new.
+ */
+
+ total_time: total.time,
+ total_count: total.count,
+ success_count: total.success,
+ failed_count: total.failed,
+ skipped_count: total.skipped,
+ error_count: total.error,
+ };
},
[types.TOGGLE_LOADING](state) {
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js
index bcf5c147916..af79521d68a 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/state.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js
@@ -1,13 +1,7 @@
-export default ({
- fullReportEndpoint = '',
- summaryEndpoint = '',
- useBuildSummaryReport = false,
-}) => ({
+export default ({ summaryEndpoint = '', suiteEndpoint = '' }) => ({
summaryEndpoint,
- fullReportEndpoint,
+ suiteEndpoint,
testReports: {},
selectedSuiteIndex: null,
- hasFullReport: false,
isLoading: false,
- useBuildSummaryReport,
});
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 9dbc8073d3a..2e08001f6b3 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -1,8 +1,7 @@
import { pickBy } from 'lodash';
import { SUPPORTED_FILTER_PARAMETERS } from './constants';
+// eslint-disable-next-line import/prefer-default-export
export const validateParams = params => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
};
-
-export default () => {};
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index aeb69fb1c05..605859cfb6a 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -100,6 +100,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
name="password"
class="form-control"
type="password"
+ data-qa-selector="password_confirmation_field"
aria-labelledby="input-label"
/>
<input
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index feb83e07607..58025381cb2 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -3,7 +3,7 @@ import { escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
export default {
components: {
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 21cc27cb1ce..6822fa8f7c7 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
-import flash from '../flash';
+import { deprecatedCreateFlash as flash } from '../flash';
import { parseBoolean } from '~/lib/utils/common_utils';
import TimezoneDropdown, {
formatTimezone,
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index a31034361a8..70fce4a4d09 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -2,10 +2,10 @@
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
deleted file mode 100644
index f5cd1c3cc3e..00000000000
--- a/app/assets/javascripts/project_fork.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import $ from 'jquery';
-
-export default () => {
- $('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
- if ($(this).hasClass('disabled')) return false;
-
- return $('.js-fork-content').toggleClass('hidden');
- });
-};
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index 5395e14cc79..12c77b09b64 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
const tooltipTitles = {
group: {
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 2b2c365dd54..788553636f9 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -54,11 +54,11 @@ const projectSelect = () => {
this.groupId,
query.term,
{
- search_namespaces: true,
with_issues_enabled: this.withIssuesEnabled,
with_merge_requests_enabled: this.withMergeRequestsEnabled,
with_shared: this.withShared,
include_subgroups: this.includeProjectsInSubgroups,
+ order_by: 'similarity',
},
projectsCallback,
);
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index f0832bd36a5..927501748a5 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/browser';
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue
new file mode 100644
index 00000000000..4b27c5e3d30
--- /dev/null
+++ b/app/assets/javascripts/projects/components/project_delete_button.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlAlert, GlSprintf } from '@gitlab/ui';
+import { __ } from '~/locale';
+import SharedDeleteButton from './shared/delete_button.vue';
+
+export default {
+ components: {
+ GlSprintf,
+ GlAlert,
+ SharedDeleteButton,
+ },
+ props: {
+ confirmPhrase: {
+ type: String,
+ required: true,
+ },
+ formPath: {
+ type: String,
+ required: true,
+ },
+ },
+ strings: {
+ alertTitle: __('You are about to permanently delete this project'),
+ alertBody: __(
+ 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its respositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.',
+ ),
+ modalBody: __(
+ "This action cannot be undone. You will lose the project's respository and all conent: issues, merge requests, etc.",
+ ),
+ },
+};
+</script>
+
+<template>
+ <shared-delete-button v-bind="{ confirmPhrase, formPath }">
+ <template #modal-body>
+ <gl-alert
+ class="gl-mb-5"
+ variant="danger"
+ :title="$options.strings.alertTitle"
+ :dismissible="false"
+ >
+ <gl-sprintf :message="$options.strings.alertBody">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ <p>{{ $options.strings.modalBody }}</p>
+ </template>
+ </shared-delete-button>
+</template>
diff --git a/app/assets/javascripts/projects/components/remove_modal.vue b/app/assets/javascripts/projects/components/remove_modal.vue
deleted file mode 100644
index 37f58efcb30..00000000000
--- a/app/assets/javascripts/projects/components/remove_modal.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<script>
-import { GlModal, GlModalDirective, GlSprintf, GlFormInput, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-import { rstrip } from '~/lib/utils/common_utils';
-import csrf from '~/lib/utils/csrf';
-
-export default {
- components: {
- GlModal,
- GlSprintf,
- GlFormInput,
- GlButton,
- },
- directives: {
- GlModal: GlModalDirective,
- },
- props: {
- confirmPhrase: {
- type: String,
- required: true,
- },
- warningMessage: {
- type: String,
- required: true,
- },
- formPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- userInput: null,
- };
- },
- computed: {
- buttonDisabled() {
- return rstrip(this.userInput) !== this.confirmPhrase;
- },
- csrfToken() {
- return csrf.token;
- },
- },
- methods: {
- submitForm() {
- this.$refs.form.submit();
- },
- },
- strings: {
- removeProject: __('Remove project'),
- title: __('Confirmation required'),
- confirm: __('Confirm'),
- dataLoss: __(
- 'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.',
- ),
- confirmText: __('Please type %{phrase_code} to proceed or close this modal to cancel.'),
- },
- modalId: 'remove-project-modal',
-};
-</script>
-
-<template>
- <form ref="form" :action="formPath" method="post">
- <input type="hidden" name="_method" value="delete" />
- <input :value="csrfToken" type="hidden" name="authenticity_token" />
- <gl-button v-gl-modal="$options.modalId" category="primary" variant="danger">{{
- $options.strings.removeProject
- }}</gl-button>
- <gl-modal
- ref="removeModal"
- :modal-id="$options.modalId"
- size="sm"
- ok-variant="danger"
- footer-class="bg-gray-light gl-p-5"
- >
- <template #modal-title>{{ $options.strings.title }}</template>
- <template #modal-footer>
- <div class="gl-w-full gl-display-flex gl-just-content-start gl-m-0">
- <gl-button
- :disabled="buttonDisabled"
- category="primary"
- variant="danger"
- @click="submitForm"
- >
- {{ $options.strings.confirm }}
- </gl-button>
- </div>
- </template>
- <div>
- <p class="gl-text-red-500 gl-font-weight-bold">{{ warningMessage }}</p>
- <p class="gl-mb-0">{{ $options.strings.dataLoss }}</p>
- <p>
- <gl-sprintf :message="$options.strings.confirmText">
- <template #phrase_code>
- <code>{{ confirmPhrase }}</code>
- </template>
- </gl-sprintf>
- </p>
- <gl-form-input
- id="confirm_name_input"
- v-model="userInput"
- name="confirm_name_input"
- type="text"
- />
- </div>
- </gl-modal>
- </form>
-</template>
diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue
new file mode 100644
index 00000000000..e3f4500d404
--- /dev/null
+++ b/app/assets/javascripts/projects/components/shared/delete_button.vue
@@ -0,0 +1,101 @@
+<script>
+import { uniqueId } from 'lodash';
+import { GlModal, GlModalDirective, GlFormInput, GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+
+export default {
+ components: {
+ GlModal,
+ GlFormInput,
+ GlButton,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ confirmPhrase: {
+ type: String,
+ required: true,
+ },
+ formPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ userInput: null,
+ modalId: uniqueId('delete-project-modal-'),
+ };
+ },
+ computed: {
+ confirmDisabled() {
+ return this.userInput !== this.confirmPhrase;
+ },
+ csrfToken() {
+ return csrf.token;
+ },
+ modalActionProps() {
+ return {
+ primary: {
+ text: __('Yes, delete project'),
+ attributes: [{ variant: 'danger' }, { disabled: this.confirmDisabled }],
+ },
+ cancel: {
+ text: __('Cancel, keep project'),
+ },
+ };
+ },
+ },
+ methods: {
+ submitForm() {
+ this.$refs.form.submit();
+ },
+ },
+ strings: {
+ deleteProject: __('Delete project'),
+ title: __('Delete project. Are you ABSOLUTELY SURE?'),
+ confirmText: __('Please type the following to confirm:'),
+ },
+};
+</script>
+
+<template>
+ <form ref="form" :action="formPath" method="post">
+ <input type="hidden" name="_method" value="delete" />
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+
+ <gl-button v-gl-modal="modalId" category="primary" variant="danger">{{
+ $options.strings.deleteProject
+ }}</gl-button>
+
+ <gl-modal
+ ref="removeModal"
+ :modal-id="modalId"
+ size="sm"
+ ok-variant="danger"
+ footer-class="gl-bg-gray-10 gl-p-5"
+ title-class="gl-text-red-500"
+ :action-primary="modalActionProps.primary"
+ :action-cancel="modalActionProps.cancel"
+ @ok="submitForm"
+ >
+ <template #modal-title>{{ $options.strings.title }}</template>
+ <div>
+ <slot name="modal-body"></slot>
+ <p class="gl-mb-1">{{ $options.strings.confirmText }}</p>
+ <p>
+ <code>{{ confirmPhrase }}</code>
+ </p>
+ <gl-form-input
+ id="confirm_name_input"
+ v-model="userInput"
+ name="confirm_name_input"
+ type="text"
+ />
+ <slot name="modal-footer"></slot>
+ </div>
+ </gl-modal>
+ </form>
+</template>
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
index e553599357c..ee4a00dbc75 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue
@@ -1,7 +1,7 @@
<script>
+import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
import WelcomePage from './welcome.vue';
import LegacyContainer from './legacy_container.vue';
-import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import blankProjectIllustration from '../illustrations/blank-project.svg';
diff --git a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
index ea22818da0e..cd9a72996cf 100644
--- a/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
+++ b/app/assets/javascripts/projects/experiment_new_project_creation/components/welcome.vue
@@ -1,6 +1,6 @@
<script>
-import Tracking from '~/tracking';
import { GlPopover } from '@gitlab/ui';
+import Tracking from '~/tracking';
import LegacyContainer from './legacy_container.vue';
const trackingMixin = Tracking.mixin(gon.tracking_data);
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index cdf03a5013f..0777dddfc19 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -1,7 +1,7 @@
<script>
import dateFormat from 'dateformat';
-import { __, sprintf } from '~/locale';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import { __, sprintf } from '~/locale';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import StatisticsList from './statistics_list.vue';
import PipelinesAreaChart from './pipelines_area_chart.vue';
diff --git a/app/assets/javascripts/projects/project_remove_modal.js b/app/assets/javascripts/projects/project_delete_button.js
index dbdad1bf6f1..aa7fc31d307 100644
--- a/app/assets/javascripts/projects/project_remove_modal.js
+++ b/app/assets/javascripts/projects/project_delete_button.js
@@ -1,21 +1,20 @@
import Vue from 'vue';
-import RemoveProjectModal from './components/remove_modal.vue';
+import ProjectDeleteButton from './components/project_delete_button.vue';
-export default (selector = '#js-confirm-project-remove') => {
+export default (selector = '#js-project-delete-button') => {
const el = document.querySelector(selector);
if (!el) return;
- const { formPath, confirmPhrase, warningMessage } = el.dataset;
+ const { confirmPhrase, formPath } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
render(createElement) {
- return createElement(RemoveProjectModal, {
+ return createElement(ProjectDeleteButton, {
props: {
confirmPhrase,
- warningMessage,
formPath,
},
});
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index ebf745fd046..ec0a83b5736 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
import { convertToTitleCase, humanize, slugify } from '../lib/utils/text_utility';
-import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates';
let hasUserDefinedProjectPath = false;
let hasUserDefinedProjectName = false;
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
new file mode 100644
index 00000000000..4dbf6675357
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -0,0 +1,524 @@
+/* eslint-disable no-underscore-dangle, class-methods-use-this */
+import { escape, find, countBy } from 'lodash';
+import axios from '~/lib/utils/axios_utils';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import { n__, s__, __ } from '~/locale';
+import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
+
+export default class AccessDropdown {
+ constructor(options) {
+ const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
+ this.options = options;
+ this.hasLicense = hasLicense;
+ this.groups = [];
+ this.accessLevel = accessLevel;
+ this.accessLevelsData = accessLevelsData.roles;
+ this.$dropdown = $dropdown;
+ this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
+ this.usersPath = '/-/autocomplete/users.json';
+ this.groupsPath = '/-/autocomplete/project_groups.json';
+ this.defaultLabel = this.$dropdown.data('defaultLabel');
+
+ this.setSelectedItems([]);
+ this.persistPreselectedItems();
+
+ this.noOneObj = this.accessLevelsData.find(level => level.id === ACCESS_LEVEL_NONE);
+
+ this.initDropdown();
+ }
+
+ initDropdown() {
+ const { onSelect, onHide } = this.options;
+ this.$dropdown.glDropdown({
+ data: this.getData.bind(this),
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ multiSelect: this.$dropdown.hasClass('js-multiselect'),
+ renderRow: this.renderRow.bind(this),
+ toggleLabel: this.toggleLabel.bind(this),
+ hidden() {
+ if (onHide) {
+ onHide();
+ }
+ },
+ clicked: options => {
+ const { $el, e } = options;
+ const item = options.selectedObj;
+
+ e.preventDefault();
+
+ if (!this.hasLicense) {
+ // We're not multiselecting quite yet with FOSS:
+ // remove all preselected items before selecting this item
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
+ this.accessLevelsData.forEach(level => {
+ this.removeSelectedItem(level);
+ });
+ }
+
+ if ($el.is('.is-active')) {
+ if (this.noOneObj) {
+ if (item.id === this.noOneObj.id && this.hasLicense) {
+ // remove all others selected items
+ this.accessLevelsData.forEach(level => {
+ if (level.id !== item.id) {
+ this.removeSelectedItem(level);
+ }
+ });
+
+ // remove selected item visually
+ this.$wrap.find(`.item-${item.type}`).removeClass('is-active');
+ } else {
+ const $noOne = this.$wrap.find(
+ `.is-active.item-${item.type}[data-role-id="${this.noOneObj.id}"]`,
+ );
+ if ($noOne.length) {
+ $noOne.removeClass('is-active');
+ this.removeSelectedItem(this.noOneObj);
+ }
+ }
+ }
+
+ // make element active right away
+ $el.addClass(`is-active item-${item.type}`);
+
+ // Add "No one"
+ this.addSelectedItem(item);
+ } else {
+ this.removeSelectedItem(item);
+ }
+
+ if (onSelect) {
+ onSelect(item, $el, this);
+ }
+ },
+ });
+
+ this.$dropdown.find('.dropdown-toggle-text').text(this.toggleLabel());
+ }
+
+ persistPreselectedItems() {
+ const itemsToPreselect = this.$dropdown.data('preselectedItems');
+
+ if (!itemsToPreselect || !itemsToPreselect.length) {
+ return;
+ }
+
+ const persistedItems = itemsToPreselect.map(item => {
+ const persistedItem = { ...item };
+ persistedItem.persisted = true;
+ return persistedItem;
+ });
+
+ this.setSelectedItems(persistedItems);
+ }
+
+ setSelectedItems(items = []) {
+ this.items = items;
+ }
+
+ getSelectedItems() {
+ return this.items.filter(item => !item._destroy);
+ }
+
+ getAllSelectedItems() {
+ return this.items;
+ }
+
+ // Return dropdown as input data ready to submit
+ getInputData() {
+ const selectedItems = this.getAllSelectedItems();
+
+ const accessLevels = selectedItems.map(item => {
+ const obj = {};
+
+ if (typeof item.id !== 'undefined') {
+ obj.id = item.id;
+ }
+
+ if (typeof item._destroy !== 'undefined') {
+ obj._destroy = item._destroy;
+ }
+
+ if (item.type === LEVEL_TYPES.ROLE) {
+ obj.access_level = item.access_level;
+ } else if (item.type === LEVEL_TYPES.USER) {
+ obj.user_id = item.user_id;
+ } else if (item.type === LEVEL_TYPES.GROUP) {
+ obj.group_id = item.group_id;
+ }
+
+ return obj;
+ });
+
+ return accessLevels;
+ }
+
+ addSelectedItem(selectedItem) {
+ let itemToAdd = {};
+
+ let index = -1;
+ let alreadyAdded = false;
+ const selectedItems = this.getAllSelectedItems();
+
+ // Compare IDs based on selectedItem.type
+ selectedItems.forEach((item, i) => {
+ let comparator;
+ switch (selectedItem.type) {
+ case LEVEL_TYPES.ROLE:
+ comparator = LEVEL_ID_PROP.ROLE;
+ // If the item already exists, just use it
+ if (item[comparator] === selectedItem.id) {
+ alreadyAdded = true;
+ }
+ break;
+ case LEVEL_TYPES.GROUP:
+ comparator = LEVEL_ID_PROP.GROUP;
+ break;
+ case LEVEL_TYPES.USER:
+ comparator = LEVEL_ID_PROP.USER;
+ break;
+ default:
+ break;
+ }
+
+ if (selectedItem.id === item[comparator]) {
+ index = i;
+ }
+ });
+
+ if (alreadyAdded) {
+ return;
+ }
+
+ if (index !== -1 && selectedItems[index]._destroy) {
+ delete selectedItems[index]._destroy;
+ return;
+ }
+
+ itemToAdd.type = selectedItem.type;
+
+ if (selectedItem.type === LEVEL_TYPES.USER) {
+ itemToAdd = {
+ user_id: selectedItem.id,
+ name: selectedItem.name || '_name1',
+ username: selectedItem.username || '_username1',
+ avatar_url: selectedItem.avatar_url || '_avatar_url1',
+ type: LEVEL_TYPES.USER,
+ };
+ } else if (selectedItem.type === LEVEL_TYPES.ROLE) {
+ itemToAdd = {
+ access_level: selectedItem.id,
+ type: LEVEL_TYPES.ROLE,
+ };
+ } else if (selectedItem.type === LEVEL_TYPES.GROUP) {
+ itemToAdd = {
+ group_id: selectedItem.id,
+ type: LEVEL_TYPES.GROUP,
+ };
+ }
+
+ this.items.push(itemToAdd);
+ }
+
+ removeSelectedItem(itemToDelete) {
+ let index = -1;
+ const selectedItems = this.getAllSelectedItems();
+
+ // To find itemToDelete on selectedItems, first we need the index
+ selectedItems.every((item, i) => {
+ if (item.type !== itemToDelete.type) {
+ return true;
+ }
+
+ if (item.type === LEVEL_TYPES.USER && item.user_id === itemToDelete.id) {
+ index = i;
+ } else if (item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) {
+ index = i;
+ } else if (item.type === LEVEL_TYPES.GROUP && item.group_id === itemToDelete.id) {
+ index = i;
+ }
+
+ // Break once we have index set
+ return !(index > -1);
+ });
+
+ // if ItemToDelete is not really selected do nothing
+ if (index === -1) {
+ return;
+ }
+
+ if (selectedItems[index].persisted) {
+ // If we toggle an item that has been already marked with _destroy
+ if (selectedItems[index]._destroy) {
+ delete selectedItems[index]._destroy;
+ } else {
+ selectedItems[index]._destroy = '1';
+ }
+ } else {
+ selectedItems.splice(index, 1);
+ }
+ }
+
+ toggleLabel() {
+ const currentItems = this.getSelectedItems();
+ const $dropdownToggleText = this.$dropdown.find('.dropdown-toggle-text');
+
+ if (currentItems.length === 0) {
+ $dropdownToggleText.addClass('is-default');
+ return this.defaultLabel;
+ }
+
+ $dropdownToggleText.removeClass('is-default');
+
+ if (currentItems.length === 1 && currentItems[0].type === LEVEL_TYPES.ROLE) {
+ const roleData = this.accessLevelsData.find(data => data.id === currentItems[0].access_level);
+ return roleData.text;
+ }
+
+ const labelPieces = [];
+ const counts = countBy(currentItems, item => item.type);
+
+ if (counts[LEVEL_TYPES.ROLE] > 0) {
+ labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
+ }
+
+ if (counts[LEVEL_TYPES.USER] > 0) {
+ labelPieces.push(n__('1 user', '%d users', counts[LEVEL_TYPES.USER]));
+ }
+
+ if (counts[LEVEL_TYPES.GROUP] > 0) {
+ labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
+ }
+
+ return labelPieces.join(', ');
+ }
+
+ getData(query, callback) {
+ if (this.hasLicense) {
+ Promise.all([
+ this.getUsers(query),
+ this.groupsData ? Promise.resolve(this.groupsData) : this.getGroups(),
+ ])
+ .then(([usersResponse, groupsResponse]) => {
+ this.groupsData = groupsResponse;
+ callback(this.consolidateData(usersResponse.data, groupsResponse.data));
+ })
+ .catch(() => Flash(__('Failed to load groups & users.')));
+ } else {
+ callback(this.consolidateData());
+ }
+ }
+
+ consolidateData(usersResponse = [], groupsResponse = []) {
+ let consolidatedData = [];
+
+ // ID property is handled differently locally from the server
+ //
+ // For Groups
+ // In dropdown: `id`
+ // For submit: `group_id`
+ //
+ // For Roles
+ // In dropdown: `id`
+ // For submit: `access_level`
+ //
+ // For Users
+ // In dropdown: `id`
+ // For submit: `user_id`
+
+ /*
+ * Build roles
+ */
+ const roles = this.accessLevelsData.map(level => {
+ /* eslint-disable no-param-reassign */
+ // This re-assignment is intentional as
+ // level.type property is being used in removeSelectedItem()
+ // for comparision, and accessLevelsData is provided by
+ // gon.create_access_levels which doesn't have `type` included.
+ // See this discussion https://gitlab.com/gitlab-org/gitlab/merge_requests/1629#note_31285823
+ level.type = LEVEL_TYPES.ROLE;
+ return level;
+ });
+
+ if (roles.length) {
+ consolidatedData = consolidatedData.concat(
+ [{ type: 'header', content: s__('AccessDropdown|Roles') }],
+ roles,
+ );
+ }
+
+ if (this.hasLicense) {
+ const map = [];
+ const selectedItems = this.getSelectedItems();
+ /*
+ * Build groups
+ */
+ const groups = groupsResponse.map(group => ({
+ ...group,
+ type: LEVEL_TYPES.GROUP,
+ }));
+
+ /*
+ * Build users
+ */
+ const users = selectedItems
+ .filter(item => item.type === LEVEL_TYPES.USER)
+ .map(item => {
+ // Save identifiers for easy-checking more later
+ map.push(LEVEL_TYPES.USER + item.user_id);
+
+ return {
+ id: item.user_id,
+ name: item.name,
+ username: item.username,
+ avatar_url: item.avatar_url,
+ type: LEVEL_TYPES.USER,
+ };
+ });
+
+ // Has to be checked against server response
+ // because the selected item can be in filter results
+ usersResponse.forEach(response => {
+ // Add is it has not been added
+ if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) {
+ const user = { ...response };
+ user.type = LEVEL_TYPES.USER;
+ users.push(user);
+ }
+ });
+
+ if (groups.length) {
+ if (roles.length) {
+ consolidatedData = consolidatedData.concat([{ type: 'divider' }]);
+ }
+
+ consolidatedData = consolidatedData.concat(
+ [{ type: 'header', content: s__('AccessDropdown|Groups') }],
+ groups,
+ );
+ }
+
+ if (users.length) {
+ consolidatedData = consolidatedData.concat(
+ [{ type: 'divider' }],
+ [{ type: 'header', content: s__('AccessDropdown|Users') }],
+ users,
+ );
+ }
+ }
+
+ return consolidatedData;
+ }
+
+ getUsers(query) {
+ return axios.get(this.buildUrl(gon.relative_url_root, this.usersPath), {
+ params: {
+ search: query,
+ per_page: 20,
+ active: true,
+ project_id: gon.current_project_id,
+ push_code: true,
+ },
+ });
+ }
+
+ getGroups() {
+ return axios.get(this.buildUrl(gon.relative_url_root, this.groupsPath), {
+ params: {
+ project_id: gon.current_project_id,
+ },
+ });
+ }
+
+ buildUrl(urlRoot, url) {
+ let newUrl;
+ if (urlRoot != null) {
+ newUrl = urlRoot.replace(/\/$/, '') + url;
+ }
+ return newUrl;
+ }
+
+ renderRow(item) {
+ let criteria = {};
+ let groupRowEl;
+
+ // Dectect if the current item is already saved so we can add
+ // the `is-active` class so the item looks as marked
+ switch (item.type) {
+ case LEVEL_TYPES.USER:
+ criteria = { user_id: item.id };
+ break;
+ case LEVEL_TYPES.ROLE:
+ criteria = { access_level: item.id };
+ break;
+ case LEVEL_TYPES.GROUP:
+ criteria = { group_id: item.id };
+ break;
+ default:
+ break;
+ }
+
+ const isActive = find(this.getSelectedItems(), criteria) ? 'is-active' : '';
+
+ switch (item.type) {
+ case LEVEL_TYPES.USER:
+ groupRowEl = this.userRowHtml(item, isActive);
+ break;
+ case LEVEL_TYPES.ROLE:
+ groupRowEl = this.roleRowHtml(item, isActive);
+ break;
+ case LEVEL_TYPES.GROUP:
+ groupRowEl = this.groupRowHtml(item, isActive);
+ break;
+ default:
+ groupRowEl = '';
+ break;
+ }
+
+ return groupRowEl;
+ }
+
+ userRowHtml(user, isActive) {
+ const isActiveClass = isActive || '';
+
+ return `
+ <li>
+ <a href="#" class="${isActiveClass}">
+ <img src="${user.avatar_url}" class="avatar avatar-inline" width="30">
+ <strong class="dropdown-menu-user-full-name">${escape(user.name)}</strong>
+ <span class="dropdown-menu-user-username">${user.username}</span>
+ </a>
+ </li>
+ `;
+ }
+
+ groupRowHtml(group, isActive) {
+ const isActiveClass = isActive || '';
+ const avatarEl = group.avatar_url
+ ? `<img src="${group.avatar_url}" class="avatar avatar-inline" width="30">`
+ : '';
+
+ return `
+ <li>
+ <a href="#" class="${isActiveClass}">
+ ${avatarEl}
+ <span class="dropdown-menu-group-groupname">${group.name}</span>
+ </a>
+ </li>
+ `;
+ }
+
+ roleRowHtml(role, isActive) {
+ const isActiveClass = isActive || '';
+
+ return `
+ <li>
+ <a href="#" class="${isActiveClass} item-${role.type}" data-role-id="${role.id}">
+ ${role.text}
+ </a>
+ </li>
+ `;
+ }
+}
diff --git a/app/assets/javascripts/projects/settings/constants.js b/app/assets/javascripts/projects/settings/constants.js
new file mode 100644
index 00000000000..fadb1f4f178
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/constants.js
@@ -0,0 +1,13 @@
+export const LEVEL_TYPES = {
+ ROLE: 'role',
+ USER: 'user',
+ GROUP: 'group',
+};
+
+export const LEVEL_ID_PROP = {
+ ROLE: 'access_level',
+ USER: 'user_id',
+ GROUP: 'group_id',
+};
+
+export const ACCESS_LEVEL_NONE = 0;
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 43c20fea43e..0b7433d6aaa 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlFormSelect, GlToggle, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -13,7 +13,7 @@ export default {
},
components: {
ClipboardButton,
- GlDeprecatedButton,
+ GlButton,
GlFormSelect,
GlToggle,
GlLoadingIcon,
@@ -157,12 +157,14 @@ export default {
}}
</span>
</template>
- <gl-deprecated-button
+ <gl-button
variant="success"
+ class="gl-mt-5"
:disabled="isTemplateSaving"
@click="onSaveTemplate"
- >{{ __('Save template') }}</gl-deprecated-button
>
+ {{ __('Save template') }}
+ </gl-button>
</div>
</div>
</div>
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 571d305a50c..e691f675e59 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
@@ -3,7 +3,7 @@ import Visibility from 'visibilityjs';
import { GlLoadingIcon } from '@gitlab/ui';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
import Poll from '~/lib/utils/poll';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { __, s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import CommitPipelineService from '../services/commit_pipeline_service';
diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
index 941a05583ad..05e769f5fc8 100644
--- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
+++ b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
@@ -1,20 +1,14 @@
<script>
-import {
- GlDeprecatedButton,
- GlFormGroup,
- GlFormInput,
- GlModal,
- GlModalDirective,
-} from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormInput, GlModal, GlModalDirective } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
export default {
copyToClipboard: __('Copy'),
components: {
- GlDeprecatedButton,
+ GlButton,
GlFormGroup,
GlFormInput,
GlModal,
@@ -131,20 +125,13 @@ export default {
)
}}
</gl-modal>
- <gl-deprecated-button
- v-gl-modal.authKeyModal
- class="js-reset-auth-key"
- :disabled="disabled"
- >{{ __('Reset key') }}</gl-deprecated-button
- >
+ <gl-button v-gl-modal.authKeyModal class="js-reset-auth-key" :disabled="disabled">{{
+ __('Reset key')
+ }}</gl-button>
</template>
- <gl-deprecated-button
- v-else
- :disabled="disabled"
- class="js-reset-auth-key"
- @click="resetKey"
- >{{ __('Generate key') }}</gl-deprecated-button
- >
+ <gl-button v-else :disabled="disabled" class="js-reset-auth-key" @click="resetKey">{{
+ __('Generate key')
+ }}</gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
index 59d47ae4155..7fc1b18bf71 100644
--- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
+++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js
@@ -87,16 +87,15 @@ export default class PrometheusMetrics {
if (totalMonitoredMetrics === 0) {
const emptyCommonMetricsText = sprintf(
- s__(
- 'PrometheusService|<p class="text-tertiary">No <a href="%{docsUrl}">common metrics</a> were found</p>',
- ),
+ s__('PrometheusService|No %{docsUrlStart}common metrics%{docsUrlEnd} were found'),
{
- docsUrl: this.helpMetricsPath,
+ docsUrlStart: `<a href="${this.helpMetricsPath}">`,
+ docsUrlEnd: '</a>',
},
false,
);
this.$monitoredMetricsEmpty.empty();
- this.$monitoredMetricsEmpty.append(emptyCommonMetricsText);
+ this.$monitoredMetricsEmpty.append(`<p class="text-tertiary">${emptyCommonMetricsText}</p>`);
this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
} else {
const metricsCountText = sprintf(
diff --git a/app/assets/javascripts/protected_branches/constants.js b/app/assets/javascripts/protected_branches/constants.js
new file mode 100644
index 00000000000..a17ae6811b7
--- /dev/null
+++ b/app/assets/javascripts/protected_branches/constants.js
@@ -0,0 +1,18 @@
+export const ACCESS_LEVELS = {
+ MERGE: 'merge_access_levels',
+ PUSH: 'push_access_levels',
+};
+
+export const LEVEL_TYPES = {
+ ROLE: 'role',
+ USER: 'user',
+ GROUP: 'group',
+};
+
+export const LEVEL_ID_PROP = {
+ ROLE: 'access_level',
+ USER: 'user_id',
+ GROUP: 'group_id',
+};
+
+export const ACCESS_LEVEL_NONE = 0;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
deleted file mode 100644
index 41e295387ae..00000000000
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { __ } from '~/locale';
-
-export default class ProtectedBranchAccessDropdown {
- constructor(options) {
- this.options = options;
- this.initDropdown();
- }
-
- initDropdown() {
- const { $dropdown, data, onSelect } = this.options;
- $dropdown.glDropdown({
- data,
- selectable: true,
- inputId: $dropdown.data('inputId'),
- fieldName: $dropdown.data('fieldName'),
- toggleLabel(item, $el) {
- if ($el.is('.is-active')) {
- return item.text;
- }
- return __('Select');
- },
- clicked(options) {
- options.e.preventDefault();
- onSelect();
- },
- });
- }
-}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 16ecd5523d6..5ccffe9700e 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,41 +1,62 @@
import $ from 'jquery';
-import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
-import CreateItemDropdown from '../create_item_dropdown';
-import AccessorUtilities from '../lib/utils/accessor';
+import AccessDropdown from '~/projects/settings/access_dropdown';
+import axios from '~/lib/utils/axios_utils';
+import AccessorUtilities from '~/lib/utils/accessor';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import CreateItemDropdown from '~/create_item_dropdown';
+import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import { __ } from '~/locale';
export default class ProtectedBranchCreate {
- constructor() {
+ constructor(options) {
+ this.hasLicense = options.hasLicense;
+
this.$form = $('.js-new-protected-branch');
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.currentProjectUserDefaults = {};
this.buildDropdowns();
+ this.$codeOwnerToggle = this.$form.find('.js-code-owner-toggle');
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ if (this.hasLicense) {
+ this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this));
+ }
+ this.$form.on('submit', this.onFormSubmit.bind(this));
+ }
+
+ onCodeOwnerToggleClick() {
+ this.$codeOwnerToggle.toggleClass('is-checked');
}
buildDropdowns() {
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
- const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown
- this.protectedBranchMergeAccessDropdown = new ProtectedBranchAccessDropdown({
+ this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToMergeDropdown,
- data: gon.merge_access_levels,
+ accessLevelsData: gon.merge_access_levels,
onSelect: this.onSelectCallback,
+ accessLevel: ACCESS_LEVELS.MERGE,
+ hasLicense: this.hasLicense,
});
// Allowed to Push dropdown
- this.protectedBranchPushAccessDropdown = new ProtectedBranchAccessDropdown({
+ this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToPushDropdown,
- data: gon.push_access_levels,
+ accessLevelsData: gon.push_access_levels,
onSelect: this.onSelectCallback,
+ accessLevel: ACCESS_LEVELS.PUSH,
+ hasLicense: this.hasLicense,
});
this.createItemDropdown = new CreateItemDropdown({
- $dropdown: $protectedBranchDropdown,
+ $dropdown: this.$form.find('.js-protected-branch-select'),
defaultToggleLabel: __('Protected Branch'),
fieldName: 'protected_branch[name]',
onSelect: this.onSelectCallback,
@@ -43,26 +64,66 @@ export default class ProtectedBranchCreate {
});
}
- // This will run after clicked callback
+ // Enable submit button after selecting an option
onSelect() {
- // Enable submit button
- const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
- const $allowedToMergeInput = this.$form.find(
- 'input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]',
- );
- const $allowedToPushInput = this.$form.find(
- 'input[name="protected_branch[push_access_levels_attributes][0][access_level]"]',
- );
- const completedForm = !(
- $branchInput.val() &&
- $allowedToMergeInput.length &&
- $allowedToPushInput.length
+ const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems();
+ const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems();
+ const toggle = !(
+ this.$form.find('input[name="protected_branch[name]"]').val() &&
+ $allowedToMerge.length &&
+ $allowedToPush.length
);
- this.$form.find('input[type="submit"]').prop('disabled', completedForm);
+ this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
static getProtectedBranches(term, callback) {
callback(gon.open_branches);
}
+
+ getFormData() {
+ const formData = {
+ authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
+ protected_branch: {
+ name: this.$form.find('input[name="protected_branch[name]"]').val(),
+ code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'),
+ },
+ };
+
+ Object.keys(ACCESS_LEVELS).forEach(level => {
+ const accessLevel = ACCESS_LEVELS[level];
+ const selectedItems = this[`${accessLevel}_dropdown`].getSelectedItems();
+ const levelAttributes = [];
+
+ selectedItems.forEach(item => {
+ if (item.type === LEVEL_TYPES.USER) {
+ levelAttributes.push({
+ user_id: item.user_id,
+ });
+ } else if (item.type === LEVEL_TYPES.ROLE) {
+ levelAttributes.push({
+ access_level: item.access_level,
+ });
+ } else if (item.type === LEVEL_TYPES.GROUP) {
+ levelAttributes.push({
+ group_id: item.group_id,
+ });
+ }
+ });
+
+ formData.protected_branch[`${accessLevel}_attributes`] = levelAttributes;
+ });
+
+ return formData;
+ }
+
+ onFormSubmit(e) {
+ e.preventDefault();
+
+ axios[this.$form.attr('method')](this.$form.attr('action'), this.getFormData())
+ .then(() => {
+ window.location.reload();
+ })
+ .catch(() => Flash(__('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 08d8c9919dd..1f079123081 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,78 +1,165 @@
-import flash from '../flash';
-import axios from '../lib/utils/axios_utils';
-import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
+import { find } from 'lodash';
+import AccessDropdown from '~/projects/settings/access_dropdown';
+import axios from '~/lib/utils/axios_utils';
+import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
+import { deprecatedCreateFlash as flash } from '../flash';
import { __ } from '~/locale';
export default class ProtectedBranchEdit {
constructor(options) {
+ this.hasLicense = options.hasLicense;
+
+ this.$wraps = {};
+ this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
- this.onSelectCallback = this.onSelect.bind(this);
+ this.$codeOwnerToggle = this.$wrap.find('.js-code-owner-toggle');
+
+ this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(
+ `.${ACCESS_LEVELS.MERGE}-container`,
+ );
+ this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest(
+ `.${ACCESS_LEVELS.PUSH}-container`,
+ );
this.buildDropdowns();
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ if (this.hasLicense) {
+ this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this));
+ }
+ }
+
+ onCodeOwnerToggleClick() {
+ this.$codeOwnerToggle.toggleClass('is-checked');
+ this.$codeOwnerToggle.prop('disabled', true);
+
+ const formData = {
+ code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'),
+ };
+
+ this.updateCodeOwnerApproval(formData);
+ }
+
+ updateCodeOwnerApproval(formData) {
+ axios
+ .patch(this.$wrap.data('url'), {
+ protected_branch: formData,
+ })
+ .then(() => {
+ this.$codeOwnerToggle.prop('disabled', false);
+ })
+ .catch(() => {
+ flash(__('Failed to update branch!'));
+ });
}
buildDropdowns() {
// Allowed to merge dropdown
- this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({
+ this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
+ accessLevel: ACCESS_LEVELS.MERGE,
+ accessLevelsData: gon.merge_access_levels,
$dropdown: this.$allowedToMergeDropdown,
- data: gon.merge_access_levels,
- onSelect: this.onSelectCallback,
+ onSelect: this.onSelectOption.bind(this),
+ onHide: this.onDropdownHide.bind(this),
+ hasLicense: this.hasLicense,
});
// Allowed to push dropdown
- this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({
+ this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
+ accessLevel: ACCESS_LEVELS.PUSH,
+ accessLevelsData: gon.push_access_levels,
$dropdown: this.$allowedToPushDropdown,
- data: gon.push_access_levels,
- onSelect: this.onSelectCallback,
+ onSelect: this.onSelectOption.bind(this),
+ onHide: this.onDropdownHide.bind(this),
+ hasLicense: this.hasLicense,
});
}
- onSelect() {
- const $allowedToMergeInput = this.$wrap.find(
- `input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`,
- );
- const $allowedToPushInput = this.$wrap.find(
- `input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`,
- );
+ onSelectOption() {
+ this.hasChanges = true;
+ }
- // Do not update if one dropdown has not selected any option
- if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
+ onDropdownHide() {
+ if (!this.hasChanges) {
+ return;
+ }
- this.$allowedToMergeDropdown.disable();
- this.$allowedToPushDropdown.disable();
+ this.hasChanges = true;
+ this.updatePermissions();
+ }
+
+ updatePermissions() {
+ const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
+ const accessLevelName = ACCESS_LEVELS[level];
+ const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
+ acc[`${accessLevelName}_attributes`] = inputData;
+
+ return acc;
+ }, {});
axios
.patch(this.$wrap.data('url'), {
- protected_branch: {
- merge_access_levels_attributes: [
- {
- id: this.$allowedToMergeDropdown.data('accessLevelId'),
- access_level: $allowedToMergeInput.val(),
- },
- ],
- push_access_levels_attributes: [
- {
- id: this.$allowedToPushDropdown.data('accessLevelId'),
- access_level: $allowedToPushInput.val(),
- },
- ],
- },
+ protected_branch: formData,
})
- .then(() => {
+ .then(({ data }) => {
+ this.hasChanges = false;
+
+ Object.keys(ACCESS_LEVELS).forEach(level => {
+ const accessLevelName = ACCESS_LEVELS[level];
+
+ // The data coming from server will be the new persisted *state* for each dropdown
+ this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`);
+ });
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
})
.catch(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
-
- flash(
- __('Failed to update branch!'),
- 'alert',
- document.querySelector('.js-protected-branches-list'),
- );
+ flash(__('Failed to update branch!'));
});
}
+
+ setSelectedItemsToDropdown(items = [], dropdownName) {
+ const itemsToAdd = items.map(currentItem => {
+ if (currentItem.user_id) {
+ // Do this only for users for now
+ // get the current data for selected items
+ const selectedItems = this[dropdownName].getSelectedItems();
+ const currentSelectedItem = find(selectedItems, {
+ user_id: currentItem.user_id,
+ });
+
+ return {
+ id: currentItem.id,
+ user_id: currentItem.user_id,
+ type: LEVEL_TYPES.USER,
+ persisted: true,
+ name: currentSelectedItem.name,
+ username: currentSelectedItem.username,
+ avatar_url: currentSelectedItem.avatar_url,
+ };
+ } else if (currentItem.group_id) {
+ return {
+ id: currentItem.id,
+ group_id: currentItem.group_id,
+ type: LEVEL_TYPES.GROUP,
+ persisted: true,
+ };
+ }
+
+ return {
+ id: currentItem.id,
+ access_level: currentItem.access_level,
+ type: LEVEL_TYPES.ROLE,
+ persisted: true,
+ };
+ });
+
+ this[dropdownName].setSelectedItems(itemsToAdd);
+ }
}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
index 10253c0febc..6ab9a126e76 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js
@@ -13,6 +13,7 @@ export default class ProtectedBranchEditList {
this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => {
new ProtectedBranchEdit({
$wrap: $(el),
+ hasLicense: false,
});
});
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index 70bfd71abce..157ac1c7ebd 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 flash from '../flash';
+import { deprecatedCreateFlash as flash } from '../flash';
import axios from '../lib/utils/axios_utils';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/ref/components/ref_results_section.vue b/app/assets/javascripts/ref/components/ref_results_section.vue
index 32e916052c4..c8f5c66b0c1 100644
--- a/app/assets/javascripts/ref/components/ref_results_section.vue
+++ b/app/assets/javascripts/ref/components/ref_results_section.vue
@@ -111,7 +111,7 @@ export default {
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column">
<span class="gl-font-monospace">{{ item.name }}</span>
- <span class="gl-text-gray-600">{{ item.subtitle }}</span>
+ <span class="gl-text-gray-400">{{ item.subtitle }}</span>
</div>
<gl-badge v-if="item.default" size="sm" variant="info">{{
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index 012a391a3da..e388604ed92 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -74,6 +74,18 @@ export default {
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
},
},
+ watch: {
+ // Keep the Vuex store synchronized if the parent
+ // component updates the selected ref through v-model
+ value: {
+ immediate: true,
+ handler() {
+ if (this.value !== this.selectedRef) {
+ this.setSelectedRef(this.value);
+ }
+ },
+ },
+ },
created() {
this.setProjectId(this.projectId);
this.search(this.query);
@@ -95,9 +107,9 @@ export default {
</script>
<template>
- <gl-new-dropdown class="ref-selector" @shown="focusSearchBox">
+ <gl-new-dropdown v-bind="$attrs" class="ref-selector" @shown="focusSearchBox">
<template slot="button-content">
- <span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-600" data-testid="button-content">
+ <span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-400" data-testid="button-content">
<span v-if="selectedRef" class="gl-font-monospace">{{ selectedRef }}</span>
<span v-else>{{ i18n.noRefSelected }}</span>
</span>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
index 51ba2337db6..8ec5cebbe8e 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
@@ -7,7 +7,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import DeleteButton from '../delete_button.vue';
import ListItem from '../list_item.vue';
-import DetailsRow from './details_row.vue';
+import DetailsRow from '~/registry/shared/components/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
DIGEST_LABEL,
diff --git a/app/assets/javascripts/registry/explorer/components/list_item.vue b/app/assets/javascripts/registry/explorer/components/list_item.vue
index 7b5afe8fd9d..c57645cc3a1 100644
--- a/app/assets/javascripts/registry/explorer/components/list_item.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_item.vue
@@ -70,7 +70,7 @@ export default {
</div>
<div class="gl-display-flex gl-flex-direction-column gl-flex-fill-1">
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-text-black-normal gl-font-weight-bold"
+ class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-text-body gl-font-weight-bold"
>
<div class="gl-display-flex gl-align-items-center">
<slot name="left-primary"></slot>
@@ -88,7 +88,7 @@ export default {
</div>
</div>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-font-sm gl-text-gray-500"
+ class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-font-sm gl-text-gray-300"
>
<div>
<slot name="left-secondary"></slot>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
index 8b06797c0ae..36a46ed58f4 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -15,7 +15,7 @@ import {
export default {
components: {
- GlDropdown,
+ GlDeprecatedDropdown,
GlFormGroup,
GlFormInputGroup,
ClipboardButton,
@@ -36,7 +36,7 @@ export default {
};
</script>
<template>
- <gl-dropdown
+ <gl-deprecated-dropdown
:text="$options.i18n.dropdownTitle"
variant="primary"
size="sm"
@@ -99,5 +99,5 @@ export default {
</gl-form-group>
</form>
</li>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
index 2874d89d913..102311c6062 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
@@ -71,7 +71,7 @@ export default {
>
<template #left-primary>
<router-link
- class="gl-text-black-normal gl-font-weight-bold"
+ class="gl-text-body gl-font-weight-bold"
data-testid="detailsLink"
:to="{ name: 'details', params: { id: encodedItem } }"
>
@@ -82,7 +82,7 @@ export default {
:disabled="item.deleting"
:text="item.location"
:title="item.location"
- css-class="btn-default btn-transparent btn-clipboard gl-text-gray-500"
+ css-class="btn-default btn-transparent btn-clipboard gl-text-gray-300"
/>
<gl-icon
v-if="item.failedDelete"
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
index d4ff84447bb..c7b4fd5f4b4 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
@@ -96,7 +96,7 @@ export default {
</div>
<div
v-if="imagesCount"
- class="gl-display-flex gl-align-items-center gl-mt-1 gl-mb-3 gl-text-gray-700"
+ class="gl-display-flex gl-align-items-center gl-mt-1 gl-mb-3 gl-text-gray-500"
data-testid="subheader"
>
<span class="gl-mr-3" data-testid="images-count">
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index cf811156704..b697bca6259 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -112,7 +112,7 @@ export default {
</script>
<template>
- <div v-gl-resize-observer="handleResize" class="gl-my-3 gl-w-full slide-enter-to-element">
+ <div v-gl-resize-observer="handleResize" class="gl-my-3">
<delete-alert
v-model="deleteAlertType"
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
diff --git a/app/assets/javascripts/registry/explorer/pages/index.vue b/app/assets/javascripts/registry/explorer/pages/index.vue
index 709a163d56d..4ac0bca84c1 100644
--- a/app/assets/javascripts/registry/explorer/pages/index.vue
+++ b/app/assets/javascripts/registry/explorer/pages/index.vue
@@ -4,8 +4,6 @@ export default {};
<template>
<div>
- <transition name="slide">
- <router-view ref="router-view" />
- </transition>
+ <router-view ref="router-view" />
</div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 1d353651c38..81e47073fe9 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -130,7 +130,7 @@ export default {
</script>
<template>
- <div class="w-100 slide-enter-from-element">
+ <div>
<gl-alert
v-if="showDeleteAlert"
:variant="deleteAlertType"
diff --git a/app/assets/javascripts/registry/explorer/stores/actions.js b/app/assets/javascripts/registry/explorer/stores/actions.js
index 3d73ffbd23f..9125f573aa4 100644
--- a/app/assets/javascripts/registry/explorer/stores/actions.js
+++ b/app/assets/javascripts/registry/explorer/stores/actions.js
@@ -1,5 +1,5 @@
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import * as types from './mutation_types';
import {
FETCH_IMAGES_LIST_ERROR_MESSAGE,
@@ -102,5 +102,3 @@ export const requestDeleteImage = ({ commit }, image) => {
commit(types.SET_MAIN_LOADING, false);
});
};
-
-export default () => {};
diff --git a/app/assets/javascripts/registry/settings/store/actions.js b/app/assets/javascripts/registry/settings/store/actions.js
index be1f62334fa..0530a870ecc 100644
--- a/app/assets/javascripts/registry/settings/store/actions.js
+++ b/app/assets/javascripts/registry/settings/store/actions.js
@@ -28,6 +28,3 @@ export const saveSettings = ({ dispatch, state }) => {
)
.finally(() => dispatch('toggleLoading'));
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_row.vue b/app/assets/javascripts/registry/shared/components/details_row.vue
index c4358b83e23..2e245fadead 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/details_row.vue
+++ b/app/assets/javascripts/registry/shared/components/details_row.vue
@@ -10,13 +10,29 @@ export default {
type: String,
required: true,
},
+ padding: {
+ type: String,
+ default: 'gl-py-2',
+ required: false,
+ },
+ dashed: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ borderClass() {
+ return this.dashed ? 'gl-border-b-solid gl-border-gray-100 gl-border-b-1' : '';
+ },
},
};
</script>
<template>
<div
- class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-py-2 gl-word-break-all"
+ class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all"
+ :class="[padding, borderClass]"
>
<gl-icon :name="icon" class="gl-mr-4" />
<span>
diff --git a/app/assets/javascripts/related_merge_requests/store/actions.js b/app/assets/javascripts/related_merge_requests/store/actions.js
index 69abeaaf7db..65f77f2fe19 100644
--- a/app/assets/javascripts/related_merge_requests/store/actions.js
+++ b/app/assets/javascripts/related_merge_requests/store/actions.js
@@ -1,5 +1,5 @@
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
@@ -32,6 +32,3 @@ export const fetchMergeRequests = ({ state, dispatch }) => {
createFlash(s__('Something went wrong while fetching related merge requests.'));
});
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/releases/components/app_edit.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index 01dd0638023..7b7c80a6269 100644
--- a/app/assets/javascripts/releases/components/app_edit.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -1,18 +1,17 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
-import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import AssetLinksForm from './asset_links_form.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
+import TagField from './tag_field.vue';
export default {
- name: 'ReleaseEditApp',
+ name: 'ReleaseEditNewApp',
components: {
GlFormInput,
GlFormGroup,
@@ -20,9 +19,7 @@ export default {
MarkdownField,
AssetLinksForm,
MilestoneCombobox,
- },
- directives: {
- autofocusonshow,
+ TagField,
},
mixins: [glFeatureFlagsMixin()],
computed: {
@@ -39,9 +36,9 @@ export default {
'manageMilestonesPath',
'projectId',
]),
- ...mapGetters('detail', ['isValid']),
+ ...mapGetters('detail', ['isValid', 'isExistingRelease']),
showForm() {
- return !this.isFetchingRelease && !this.fetchError;
+ return Boolean(!this.isFetchingRelease && !this.fetchError && this.release);
},
subtitleText() {
return sprintf(
@@ -55,23 +52,6 @@ export default {
false,
);
},
- tagName() {
- return this.$store.state.detail.release.tagName;
- },
- tagNameHintText() {
- return sprintf(
- __(
- 'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}',
- ),
- {
- linkStart: `<a href="${escape(
- this.updateReleaseApiDocsPath,
- )}" target="_blank" rel="noopener noreferrer">`,
- linkEnd: '</a>',
- },
- false,
- );
- },
releaseTitle: {
get() {
return this.$store.state.detail.release.name;
@@ -102,7 +82,10 @@ export default {
showAssetLinksForm() {
return this.glFeatures.releaseAssetLinkEditing;
},
- isSaveChangesDisabled() {
+ saveButtonLabel() {
+ return this.isExistingRelease ? __('Save changes') : __('Create release');
+ },
+ isFormSubmissionDisabled() {
return this.isUpdatingRelease || !this.isValid;
},
milestoneComboboxExtraLinks() {
@@ -118,53 +101,45 @@ export default {
];
},
},
- created() {
- this.fetchRelease();
+ mounted() {
+ // eslint-disable-next-line promise/catch-or-return
+ this.initializeRelease().then(() => {
+ // Focus the first non-disabled input element
+ this.$el.querySelector('input:enabled').focus();
+ });
},
methods: {
...mapActions('detail', [
- 'fetchRelease',
- 'updateRelease',
+ 'initializeRelease',
+ 'saveRelease',
'updateReleaseTitle',
'updateReleaseNotes',
'updateReleaseMilestones',
]),
+ submitForm() {
+ if (!this.isFormSubmissionDisabled) {
+ this.saveRelease();
+ }
+ },
},
};
</script>
<template>
<div class="d-flex flex-column">
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
- <form v-if="showForm" @submit.prevent="updateRelease()">
- <gl-form-group>
- <div class="row">
- <div class="col-md-6 col-lg-5 col-xl-4">
- <label for="git-ref">{{ __('Tag name') }}</label>
- <gl-form-input
- id="git-ref"
- v-model="tagName"
- type="text"
- class="form-control"
- aria-describedby="tag-name-help"
- disabled
- />
- </div>
- </div>
- <div id="tag-name-help" class="form-text text-muted" v-html="tagNameHintText"></div>
- </gl-form-group>
+ <form v-if="showForm" class="js-quick-submit" @submit.prevent="submitForm">
+ <tag-field />
<gl-form-group>
<label for="release-title">{{ __('Release title') }}</label>
<gl-form-input
id="release-title"
ref="releaseTitleInput"
v-model="releaseTitle"
- v-autofocusonshow
- autofocus
type="text"
class="form-control"
/>
</gl-form-group>
- <gl-form-group class="w-50">
+ <gl-form-group class="w-50" @keydown.enter.prevent.capture>
<label>{{ __('Milestones') }}</label>
<div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
<milestone-combobox
@@ -182,7 +157,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:add-spacing-classes="false"
- class="prepend-top-10 append-bottom-10"
+ class="gl-mt-3 gl-mb-3"
>
<template #textarea>
<textarea
@@ -193,8 +168,6 @@ export default {
data-supports-quick-actions="false"
:aria-label="__('Release notes')"
:placeholder="__('Write your release notes or drag your files here…')"
- @keydown.meta.enter="updateRelease()"
- @keydown.ctrl.enter="updateRelease()"
></textarea>
</template>
</markdown-field>
@@ -209,10 +182,11 @@ export default {
category="primary"
variant="success"
type="submit"
- :aria-label="__('Save changes')"
- :disabled="isSaveChangesDisabled"
- >{{ __('Save changes') }}</gl-button
+ :disabled="isFormSubmissionDisabled"
+ data-testid="submit-button"
>
+ {{ saveButtonLabel }}
+ </gl-button>
<gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button>
</div>
</form>
diff --git a/app/assets/javascripts/releases/components/app_new.vue b/app/assets/javascripts/releases/components/app_new.vue
deleted file mode 100644
index 563f76b3281..00000000000
--- a/app/assets/javascripts/releases/components/app_new.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-<script>
-export default {
- name: 'ReleaseNewApp',
- components: {},
-};
-</script>
-<template>
- <div></div>
-</template>
diff --git a/app/assets/javascripts/releases/components/asset_links_form.vue b/app/assets/javascripts/releases/components/asset_links_form.vue
index d0d1485d8e7..07fab840067 100644
--- a/app/assets/javascripts/releases/components/asset_links_form.vue
+++ b/app/assets/javascripts/releases/components/asset_links_form.vue
@@ -49,6 +49,12 @@ export default {
this.removeAssetLink(linkId);
this.ensureAtLeastOneLink();
},
+ updateUrl(link, newUrl) {
+ this.updateAssetLinkUrl({ linkIdToUpdate: link.id, newUrl });
+ },
+ updateName(link, newName) {
+ this.updateAssetLinkName({ linkIdToUpdate: link.id, newName });
+ },
hasDuplicateUrl(link) {
return Boolean(this.getLinkErrors(link).isDuplicate);
},
@@ -138,7 +144,9 @@ export default {
type="text"
class="form-control"
:state="isUrlValid(link)"
- @change="updateAssetLinkUrl({ linkIdToUpdate: link.id, newUrl: $event })"
+ @change="updateUrl(link, $event)"
+ @keydown.ctrl.enter="updateUrl(link, $event.target.value)"
+ @keydown.meta.enter="updateUrl(link, $event.target.value)"
/>
<template #invalid-feedback>
<span v-if="hasEmptyUrl(link)" class="invalid-feedback d-inline">
@@ -175,7 +183,9 @@ export default {
type="text"
class="form-control"
:state="isNameValid(link)"
- @change="updateAssetLinkName({ linkIdToUpdate: link.id, newName: $event })"
+ @change="updateName(link, $event)"
+ @keydown.ctrl.enter="updateName(link, $event.target.value)"
+ @keydown.meta.enter="updateName(link, $event.target.value)"
/>
<template #invalid-feedback>
<span v-if="hasEmptyName(link)" class="invalid-feedback d-inline">
diff --git a/app/assets/javascripts/releases/components/form_field_container.vue b/app/assets/javascripts/releases/components/form_field_container.vue
new file mode 100644
index 00000000000..19e275315a0
--- /dev/null
+++ b/app/assets/javascripts/releases/components/form_field_container.vue
@@ -0,0 +1,12 @@
+<script>
+export default {
+ name: 'FormFieldContainer',
+};
+</script>
+<template>
+ <div class="row">
+ <div class="col-md-6 col-lg-5 col-xl-4 gl-display-flex gl-flex-direction-column">
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/components/release_block_assets.vue b/app/assets/javascripts/releases/components/release_block_assets.vue
index ab29ceb0ce6..9583f5737df 100644
--- a/app/assets/javascripts/releases/components/release_block_assets.vue
+++ b/app/assets/javascripts/releases/components/release_block_assets.vue
@@ -1,10 +1,10 @@
<script>
import { GlTooltipDirective, GlLink, GlButton, GlCollapse, GlIcon, GlBadge } from '@gitlab/ui';
+import { difference, get } from 'lodash';
import Icon from '~/vue_shared/components/icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ASSET_LINK_TYPE } from '../constants';
import { __, s__, sprintf } from '~/locale';
-import { difference, get } from 'lodash';
export default {
name: 'ReleaseBlockAssets',
@@ -138,7 +138,7 @@ export default {
:aria-label="$options.externalLinkTooltipText"
:title="$options.externalLinkTooltipText"
data-testid="external-link-indicator"
- class="gl-ml-2 gl-flex-shrink-0 gl-flex-grow-0 gl-text-gray-600"
+ class="gl-ml-2 gl-flex-shrink-0 gl-flex-grow-0 gl-text-gray-400"
/>
</gl-link>
</li>
diff --git a/app/assets/javascripts/releases/components/release_block_author.vue b/app/assets/javascripts/releases/components/release_block_author.vue
index 94f2b1795f0..72c578068cd 100644
--- a/app/assets/javascripts/releases/components/release_block_author.vue
+++ b/app/assets/javascripts/releases/components/release_block_author.vue
@@ -1,6 +1,6 @@
<script>
-import { __, sprintf } from '~/locale';
import { GlSprintf } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
diff --git a/app/assets/javascripts/releases/components/release_block_milestone_info.vue b/app/assets/javascripts/releases/components/release_block_milestone_info.vue
index b16ae400d6b..deff673cc17 100644
--- a/app/assets/javascripts/releases/components/release_block_milestone_info.vue
+++ b/app/assets/javascripts/releases/components/release_block_milestone_info.vue
@@ -7,9 +7,9 @@ import {
GlTooltipDirective,
GlSprintf,
} from '@gitlab/ui';
+import { sum } from 'lodash';
import { __, n__, sprintf } from '~/locale';
import { MAX_MILESTONES_TO_DISPLAY } from '../constants';
-import { sum } from 'lodash';
export default {
name: 'ReleaseBlockMilestoneInfo',
diff --git a/app/assets/javascripts/releases/components/tag_field.vue b/app/assets/javascripts/releases/components/tag_field.vue
new file mode 100644
index 00000000000..ed8d6e62926
--- /dev/null
+++ b/app/assets/javascripts/releases/components/tag_field.vue
@@ -0,0 +1,20 @@
+<script>
+import { mapGetters } from 'vuex';
+import TagFieldExisting from './tag_field_existing.vue';
+import TagFieldNew from './tag_field_new.vue';
+
+export default {
+ components: {
+ TagFieldExisting,
+ TagFieldNew,
+ },
+ computed: {
+ ...mapGetters('detail', ['isExistingRelease']),
+ },
+};
+</script>
+
+<template>
+ <tag-field-existing v-if="isExistingRelease" />
+ <tag-field-new v-else />
+</template>
diff --git a/app/assets/javascripts/releases/components/tag_field_existing.vue b/app/assets/javascripts/releases/components/tag_field_existing.vue
new file mode 100644
index 00000000000..b84e713df26
--- /dev/null
+++ b/app/assets/javascripts/releases/components/tag_field_existing.vue
@@ -0,0 +1,51 @@
+<script>
+import { mapState } from 'vuex';
+import { uniqueId } from 'lodash';
+import { GlFormGroup, GlFormInput, GlLink, GlSprintf } from '@gitlab/ui';
+import FormFieldContainer from './form_field_container.vue';
+
+export default {
+ name: 'TagFieldExisting',
+ components: { GlFormGroup, GlFormInput, GlSprintf, GlLink, FormFieldContainer },
+ computed: {
+ ...mapState('detail', ['release', 'updateReleaseApiDocsPath']),
+ inputId() {
+ return uniqueId('tag-name-input-');
+ },
+ helpId() {
+ return uniqueId('tag-name-help-');
+ },
+ },
+};
+</script>
+<template>
+ <gl-form-group :label="__('Tag name')" :label-for="inputId">
+ <form-field-container>
+ <gl-form-input
+ :id="inputId"
+ :value="release.tagName"
+ type="text"
+ class="form-control"
+ :aria-describedby="helpId"
+ disabled
+ />
+ </form-field-container>
+ <template #description>
+ <div :id="helpId" data-testid="tag-name-help">
+ <gl-sprintf
+ :message="
+ __(
+ 'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="updateReleaseApiDocsPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </template>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
new file mode 100644
index 00000000000..4779feae886
--- /dev/null
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -0,0 +1,100 @@
+<script>
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { __ } from '~/locale';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import FormFieldContainer from './form_field_container.vue';
+
+export default {
+ name: 'TagFieldNew',
+ components: { GlFormGroup, GlFormInput, RefSelector, FormFieldContainer },
+ data() {
+ return {
+ // Keeps track of whether or not the user has interacted with
+ // the input field. This is used to avoid showing validation
+ // errors immediately when the page loads.
+ isInputDirty: false,
+ };
+ },
+ computed: {
+ ...mapState('detail', ['projectId', 'release', 'createFrom']),
+ ...mapGetters('detail', ['validationErrors']),
+ tagName: {
+ get() {
+ return this.release.tagName;
+ },
+ set(tagName) {
+ this.updateReleaseTagName(tagName);
+ },
+ },
+ createFromModel: {
+ get() {
+ return this.createFrom;
+ },
+ set(createFrom) {
+ this.updateCreateFrom(createFrom);
+ },
+ },
+ showTagNameValidationError() {
+ return this.isInputDirty && this.validationErrors.isTagNameEmpty;
+ },
+ tagNameInputId() {
+ return uniqueId('tag-name-input-');
+ },
+ createFromSelectorId() {
+ return uniqueId('create-from-selector-');
+ },
+ },
+ methods: {
+ ...mapActions('detail', ['updateReleaseTagName', 'updateCreateFrom']),
+ markInputAsDirty() {
+ this.isInputDirty = true;
+ },
+ },
+ translations: {
+ noRefSelected: __('No source selected'),
+ searchPlaceholder: __('Search branches, tags, and commits'),
+ dropdownHeader: __('Select source'),
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-form-group
+ :label="__('Tag name')"
+ :label-for="tagNameInputId"
+ data-testid="tag-name-field"
+ :state="!showTagNameValidationError"
+ :invalid-feedback="__('Tag name is required')"
+ >
+ <form-field-container>
+ <gl-form-input
+ :id="tagNameInputId"
+ v-model="tagName"
+ :state="!showTagNameValidationError"
+ type="text"
+ class="form-control"
+ @blur.once="markInputAsDirty"
+ />
+ </form-field-container>
+ </gl-form-group>
+ <gl-form-group
+ :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"
+ />
+ </form-field-container>
+ <template #description>
+ {{ __('Existing branch name, tag, or commit SHA') }}
+ </template>
+ </gl-form-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/releases/mount_edit.js b/app/assets/javascripts/releases/mount_edit.js
index 44530e4961a..c7385b3c57f 100644
--- a/app/assets/javascripts/releases/mount_edit.js
+++ b/app/assets/javascripts/releases/mount_edit.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import ReleaseEditApp from './components/app_edit.vue';
+import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
@@ -18,6 +18,6 @@ export default () => {
return new Vue({
el,
store,
- render: h => h(ReleaseEditApp),
+ render: h => h(ReleaseEditNewApp),
});
};
diff --git a/app/assets/javascripts/releases/mount_new.js b/app/assets/javascripts/releases/mount_new.js
index eb02c194c59..68003f6a346 100644
--- a/app/assets/javascripts/releases/mount_new.js
+++ b/app/assets/javascripts/releases/mount_new.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import ReleaseNewApp from './components/app_new.vue';
+import ReleaseEditNewApp from './components/app_edit_new.vue';
import createStore from './stores';
import createDetailModule from './stores/modules/detail';
@@ -10,11 +10,14 @@ export default () => {
modules: {
detail: createDetailModule(el.dataset),
},
+ featureFlags: {
+ releaseShowPage: Boolean(gon.features?.releaseShowPage),
+ },
});
return new Vue({
el,
store,
- render: h => h(ReleaseNewApp),
+ render: h => h(ReleaseEditNewApp),
});
};
diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js
index 2026eeba880..5b682a0ab0f 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js
@@ -1,74 +1,116 @@
import * as types from './mutation_types';
import api from '~/api';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
-import {
- convertObjectPropsToCamelCase,
- convertObjectPropsToSnakeCase,
-} from '~/lib/utils/common_utils';
-
-export const requestRelease = ({ commit }) => commit(types.REQUEST_RELEASE);
-export const receiveReleaseSuccess = ({ commit }, data) =>
- commit(types.RECEIVE_RELEASE_SUCCESS, data);
-export const receiveReleaseError = ({ commit }, error) => {
- commit(types.RECEIVE_RELEASE_ERROR, error);
- createFlash(s__('Release|Something went wrong while getting the release details'));
+import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
+
+export const initializeRelease = ({ commit, dispatch, getters }) => {
+ if (getters.isExistingRelease) {
+ // When editing an existing release,
+ // fetch the release object from the API
+ return dispatch('fetchRelease');
+ }
+
+ // When creating a new release, initialize the
+ // store with an empty release object
+ commit(types.INITIALIZE_EMPTY_RELEASE);
+ return Promise.resolve();
};
-export const fetchRelease = ({ dispatch, state }) => {
- dispatch('requestRelease');
+export const fetchRelease = ({ commit, state }) => {
+ commit(types.REQUEST_RELEASE);
return api
.release(state.projectId, state.tagName)
.then(({ data }) => {
- const release = {
- ...data,
- milestones: data.milestones || [],
- };
-
- dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true }));
+ commit(types.RECEIVE_RELEASE_SUCCESS, apiJsonToRelease(data));
})
.catch(error => {
- dispatch('receiveReleaseError', error);
+ commit(types.RECEIVE_RELEASE_ERROR, error);
+ createFlash(s__('Release|Something went wrong while getting the release details'));
});
};
+export const updateReleaseTagName = ({ commit }, tagName) =>
+ commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
+
+export const updateCreateFrom = ({ commit }, createFrom) =>
+ commit(types.UPDATE_CREATE_FROM, createFrom);
+
export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
+
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
+
export const updateReleaseMilestones = ({ commit }, milestones) =>
commit(types.UPDATE_RELEASE_MILESTONES, milestones);
-export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
-export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => {
- commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
- redirectTo(
- rootState.featureFlags.releaseShowPage ? state.release._links.self : state.releasesPagePath,
- );
+export const addEmptyAssetLink = ({ commit }) => {
+ commit(types.ADD_EMPTY_ASSET_LINK);
};
-export const receiveUpdateReleaseError = ({ commit }, error) => {
- commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error);
- createFlash(s__('Release|Something went wrong while saving the release details'));
+
+export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
+ commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
};
-export const updateRelease = ({ dispatch, state, getters }) => {
- dispatch('requestUpdateRelease');
+export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
+ commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
+};
- const { release } = state;
- const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : [];
+export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) => {
+ commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType });
+};
+
+export const removeAssetLink = ({ commit }, linkIdToRemove) => {
+ commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
+};
+
+export const receiveSaveReleaseSuccess = ({ commit, state, rootState }, release) => {
+ commit(types.RECEIVE_SAVE_RELEASE_SUCCESS);
+ redirectTo(rootState.featureFlags.releaseShowPage ? release._links.self : state.releasesPagePath);
+};
+
+export const saveRelease = ({ commit, dispatch, getters }) => {
+ commit(types.REQUEST_SAVE_RELEASE);
- const updatedRelease = convertObjectPropsToSnakeCase(
+ dispatch(getters.isExistingRelease ? 'updateRelease' : 'createRelease');
+};
+
+export const createRelease = ({ commit, dispatch, state, getters }) => {
+ const apiJson = releaseToApiJson(
{
- name: release.name,
- description: release.description,
- milestones,
+ ...state.release,
+ assets: {
+ links: getters.releaseLinksToCreate,
+ },
},
- { deep: true },
+ state.createFrom,
);
+ return api
+ .createRelease(state.projectId, apiJson)
+ .then(({ data }) => {
+ dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(data));
+ })
+ .catch(error => {
+ commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
+ createFlash(s__('Release|Something went wrong while creating a new release'));
+ });
+};
+
+export const updateRelease = ({ commit, dispatch, state, getters }) => {
+ const apiJson = releaseToApiJson({
+ ...state.release,
+ assets: {
+ links: getters.releaseLinksToCreate,
+ },
+ });
+
+ let updatedRelease = null;
+
return (
api
- .updateRelease(state.projectId, state.tagName, updatedRelease)
+ .updateRelease(state.projectId, state.tagName, apiJson)
/**
* Currently, we delete all existing links and then
@@ -86,54 +128,31 @@ export const updateRelease = ({ dispatch, state, getters }) => {
* https://gitlab.com/gitlab-org/gitlab/-/issues/208702
* is closed.
*/
+ .then(({ data }) => {
+ // Save this response since we need it later in the Promise chain
+ updatedRelease = data;
- .then(() => {
// Delete all links currently associated with this Release
return Promise.all(
getters.releaseLinksToDelete.map(l =>
- api.deleteReleaseLink(state.projectId, release.tagName, l.id),
+ api.deleteReleaseLink(state.projectId, state.release.tagName, l.id),
),
);
})
.then(() => {
// Create a new link for each link in the form
return Promise.all(
- getters.releaseLinksToCreate.map(l =>
- api.createReleaseLink(
- state.projectId,
- release.tagName,
- convertObjectPropsToSnakeCase(l, { deep: true }),
- ),
+ apiJson.assets.links.map(l =>
+ api.createReleaseLink(state.projectId, state.release.tagName, l),
),
);
})
- .then(() => dispatch('receiveUpdateReleaseSuccess'))
+ .then(() => {
+ dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(updatedRelease));
+ })
.catch(error => {
- dispatch('receiveUpdateReleaseError', error);
+ commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
+ createFlash(s__('Release|Something went wrong while saving the release details'));
})
);
};
-
-export const navigateToReleasesPage = ({ state }) => {
- redirectTo(state.releasesPagePath);
-};
-
-export const addEmptyAssetLink = ({ commit }) => {
- commit(types.ADD_EMPTY_ASSET_LINK);
-};
-
-export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
- commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
-};
-
-export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
- commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
-};
-
-export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) => {
- commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType });
-};
-
-export const removeAssetLink = ({ commit }, linkIdToRemove) => {
- commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
-};
diff --git a/app/assets/javascripts/releases/stores/modules/detail/getters.js b/app/assets/javascripts/releases/stores/modules/detail/getters.js
index 84dc2fca4be..809ed075c16 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/getters.js
@@ -2,6 +2,14 @@ import { isEmpty } from 'lodash';
import { hasContent } from '~/lib/utils/text_utility';
/**
+ * @returns {Boolean} `true` if the app is editing an existing release.
+ * `false` if the app is creating a new release.
+ */
+export const isExistingRelease = state => {
+ return Boolean(state.tagName);
+};
+
+/**
* @param {Object} link The link to test
* @returns {Boolean} `true` if the release link is empty, i.e. it has
* empty (or whitespace-only) values for both `url` and `name`.
@@ -39,6 +47,10 @@ export const validationErrors = state => {
return errors;
}
+ if (!state.release.tagName?.trim?.().length) {
+ errors.isTagNameEmpty = true;
+ }
+
// Each key of this object is a URL, and the value is an
// array of Release link objects that share this URL.
// This is used for detecting duplicate URLs.
@@ -88,5 +100,6 @@ export const validationErrors = state => {
/** Returns whether or not the release object is valid */
export const isValid = (_state, getters) => {
- return Object.values(getters.validationErrors.assets.links).every(isEmpty);
+ const errors = getters.validationErrors;
+ return Object.values(errors.assets.links).every(isEmpty) && !errors.isTagNameEmpty;
};
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
index 7b694120126..7784e0cc741 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
@@ -1,14 +1,18 @@
+export const INITIALIZE_EMPTY_RELEASE = 'INITIALIZE_EMPTY_RELEASE';
+
export const REQUEST_RELEASE = 'REQUEST_RELEASE';
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_CREATE_FROM = 'UPDATE_CREATE_FROM';
export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES';
-export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
-export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
-export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR';
+export const REQUEST_SAVE_RELEASE = 'REQUEST_SAVE_RELEASE';
+export const RECEIVE_SAVE_RELEASE_SUCCESS = 'RECEIVE_SAVE_RELEASE_SUCCESS';
+export const RECEIVE_SAVE_RELEASE_ERROR = 'RECEIVE_SAVE_RELEASE_ERROR';
export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK';
export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL';
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutations.js b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
index ca544151323..750f496665d 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
@@ -1,5 +1,5 @@
-import * as types from './mutation_types';
import { uniqueId, cloneDeep } from 'lodash';
+import * as types from './mutation_types';
import { DEFAULT_ASSET_LINK_TYPE } from '../../../constants';
const findReleaseLink = (release, id) => {
@@ -7,6 +7,18 @@ const findReleaseLink = (release, id) => {
};
export default {
+ [types.INITIALIZE_EMPTY_RELEASE](state) {
+ state.release = {
+ tagName: null,
+ name: '',
+ description: '',
+ milestones: [],
+ assets: {
+ links: [],
+ },
+ };
+ },
+
[types.REQUEST_RELEASE](state) {
state.isFetchingRelease = true;
},
@@ -22,6 +34,12 @@ export default {
state.release = undefined;
},
+ [types.UPDATE_RELEASE_TAG_NAME](state, tagName) {
+ state.release.tagName = tagName;
+ },
+ [types.UPDATE_CREATE_FROM](state, createFrom) {
+ state.createFrom = createFrom;
+ },
[types.UPDATE_RELEASE_TITLE](state, title) {
state.release.name = title;
},
@@ -33,14 +51,14 @@ export default {
state.release.milestones = milestones;
},
- [types.REQUEST_UPDATE_RELEASE](state) {
+ [types.REQUEST_SAVE_RELEASE](state) {
state.isUpdatingRelease = true;
},
- [types.RECEIVE_UPDATE_RELEASE_SUCCESS](state) {
+ [types.RECEIVE_SAVE_RELEASE_SUCCESS](state) {
state.updateError = undefined;
state.isUpdatingRelease = false;
},
- [types.RECEIVE_UPDATE_RELEASE_ERROR](state, error) {
+ [types.RECEIVE_SAVE_RELEASE_ERROR](state, error) {
state.updateError = error;
state.isUpdatingRelease = false;
},
diff --git a/app/assets/javascripts/releases/stores/modules/detail/state.js b/app/assets/javascripts/releases/stores/modules/detail/state.js
index 966c1c00ef5..a46e750df53 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/state.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/state.js
@@ -6,9 +6,9 @@ export default ({
releaseAssetsDocsPath,
manageMilestonesPath,
newMilestonePath,
+ releasesPagePath,
tagName = null,
- releasesPagePath = null,
defaultBranch = null,
}) => ({
projectId,
@@ -18,10 +18,16 @@ export default ({
releaseAssetsDocsPath,
manageMilestonesPath,
newMilestonePath,
+ releasesPagePath,
+ /**
+ * The name of the tag associated with the release, provided by the backend.
+ * When creating a new release, this value is null.
+ */
tagName,
- releasesPagePath,
+
defaultBranch,
+ createFrom: defaultBranch,
/** The Release object */
release: null,
diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/list/actions.js
index 06d13890a9d..90fba319e9f 100644
--- a/app/assets/javascripts/releases/stores/modules/list/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/list/actions.js
@@ -1,5 +1,5 @@
import * as types from './mutation_types';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import api from '~/api';
import {
@@ -43,6 +43,3 @@ export const receiveReleasesError = ({ commit }) => {
commit(types.RECEIVE_RELEASES_ERROR);
createFlash(__('An error occurred while fetching the releases. Please try again.'));
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js
new file mode 100644
index 00000000000..842a423b142
--- /dev/null
+++ b/app/assets/javascripts/releases/util.js
@@ -0,0 +1,41 @@
+import {
+ convertObjectPropsToCamelCase,
+ convertObjectPropsToSnakeCase,
+} from '~/lib/utils/common_utils';
+
+/**
+ * Converts a release object into a JSON object that can sent to the public
+ * API to create or update a release.
+ * @param {Object} release The release object to convert
+ * @param {string} createFrom The ref to create a new tag from, if necessary
+ */
+export const releaseToApiJson = (release, createFrom = null) => {
+ const name = release.name?.trim().length > 0 ? release.name.trim() : null;
+
+ const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : [];
+
+ return convertObjectPropsToSnakeCase(
+ {
+ name,
+ tagName: release.tagName,
+ ref: createFrom,
+ description: release.description,
+ milestones,
+ assets: release.assets,
+ },
+ { deep: true },
+ );
+};
+
+/**
+ * Converts a JSON release object returned by the Release API
+ * into the structure this Vue application can work with.
+ * @param {Object} json The JSON object received from the release API
+ */
+export const apiJsonToRelease = json => {
+ const release = convertObjectPropsToCamelCase(json, { deep: true });
+
+ release.milestones = release.milestones || [];
+
+ return release;
+};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/actions.js b/app/assets/javascripts/reports/accessibility_report/store/actions.js
index 446cfd79984..bb502020a06 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/actions.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/actions.js
@@ -74,6 +74,3 @@ export const receiveReportError = ({ commit, dispatch }) => {
commit(types.RECEIVE_REPORT_ERROR);
dispatch('stopPolling');
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/getters.js b/app/assets/javascripts/reports/accessibility_report/store/getters.js
index 9aff427e644..312b333a771 100644
--- a/app/assets/javascripts/reports/accessibility_report/store/getters.js
+++ b/app/assets/javascripts/reports/accessibility_report/store/getters.js
@@ -43,6 +43,3 @@ export const shouldRenderIssuesList = state =>
export const unresolvedIssues = state => state.report.existing_errors;
export const resolvedIssues = state => state.report.resolved_errors;
export const newIssues = state => state.report.new_errors;
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
index b8a8cb940e7..47f04019595 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -1,12 +1,12 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
+import { GlButton } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import { componentNames } from './issue_body';
import ReportSection from './report_section.vue';
import SummaryRow from './summary_row.vue';
import IssuesList from './issues_list.vue';
import Modal from './modal.vue';
-import { GlButton } from '@gitlab/ui';
import createStore from '../store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils';
@@ -56,7 +56,7 @@ export default {
return `${this.pipelinePath}/test_report`;
},
showViewFullReport() {
- return Boolean(this.glFeatures.junitPipelineView) && this.pipelinePath.length;
+ return this.pipelinePath.length;
},
},
created() {
@@ -116,6 +116,7 @@ export default {
<template v-if="showViewFullReport" #actionButtons>
<gl-button
:href="testTabURL"
+ target="_blank"
icon="external-link"
data-testid="group-test-reports-full-link"
class="gl-mr-3"
diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue
index 78c355ecb76..ca95db6c826 100644
--- a/app/assets/javascripts/reports/components/modal.vue
+++ b/app/assets/javascripts/reports/components/modal.vue
@@ -33,7 +33,7 @@ export default {
v-for="(field, key, index) in modalData"
v-if="field.value"
:key="index"
- class="row prepend-top-10 append-bottom-10"
+ class="row gl-mt-3 gl-mb-3"
>
<strong class="col-sm-3 text-right"> {{ field.text }}: </strong>
diff --git a/app/assets/javascripts/reports/components/modal_open_name.vue b/app/assets/javascripts/reports/components/modal_open_name.vue
index 4f81cee2a38..78e1fcb205b 100644
--- a/app/assets/javascripts/reports/components/modal_open_name.vue
+++ b/app/assets/javascripts/reports/components/modal_open_name.vue
@@ -1,7 +1,12 @@
<script>
+import { GlTooltipDirective, GlResizeObserverDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlResizeObserverDirective,
+ },
props: {
issue: {
type: Object,
@@ -13,19 +18,32 @@ export default {
required: true,
},
},
+ data: () => ({
+ tooltipTitle: '',
+ }),
+ mounted() {
+ this.updateTooltipTitle();
+ },
methods: {
...mapActions(['openModal']),
handleIssueClick() {
const { issue, status, openModal } = this;
openModal({ issue, status });
},
+ updateTooltipTitle() {
+ // Only show the tooltip if the text is truncated with an ellipsis.
+ this.tooltipTitle = this.$el.offsetWidth < this.$el.scrollWidth ? this.issue.title : '';
+ },
},
};
</script>
<template>
<button
- type="button"
- class="btn-link btn-blank text-left break-link vulnerability-name-button"
+ v-gl-tooltip="{ boundary: 'viewport' }"
+ v-gl-resize-observer-directive="updateTooltipTitle"
+ class="btn-link gl-text-truncate"
+ :aria-label="s__('Reports|Vulnerability Name')"
+ :title="tooltipTitle"
@click="handleIssueClick()"
>
{{ issue.title }}
diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js
index db8ab5ccb80..c5860db6601 100644
--- a/app/assets/javascripts/reports/store/actions.js
+++ b/app/assets/javascripts/reports/store/actions.js
@@ -85,6 +85,3 @@ export const openModal = ({ dispatch }, payload) => {
};
export const setModalData = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload);
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/reports/store/getters.js b/app/assets/javascripts/reports/store/getters.js
index 95266194acb..6345be69f6f 100644
--- a/app/assets/javascripts/reports/store/getters.js
+++ b/app/assets/javascripts/reports/store/getters.js
@@ -1,5 +1,6 @@
import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../constants';
+// eslint-disable-next-line import/prefer-default-export
export const summaryStatus = state => {
if (state.isLoading) {
return LOADING;
@@ -11,6 +12,3 @@ export const summaryStatus = state => {
return SUCCESS;
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index 45c343c3f7f..368fa029d07 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -1,12 +1,17 @@
<script>
-import { GlDropdown, GlDropdownDivider, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui';
+import {
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownDivider,
+ GlDeprecatedDropdownHeader,
+ GlDeprecatedDropdownItem,
+} from '@gitlab/ui';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '../../locale';
import Icon from '../../vue_shared/components/icon.vue';
import getRefMixin from '../mixins/get_ref';
-import getProjectShortPath from '../queries/getProjectShortPath.query.graphql';
-import getProjectPath from '../queries/getProjectPath.query.graphql';
-import getPermissions from '../queries/getPermissions.query.graphql';
+import projectShortPathQuery from '../queries/project_short_path.query.graphql';
+import projectPathQuery from '../queries/project_path.query.graphql';
+import permissionsQuery from '../queries/permissions.query.graphql';
const ROW_TYPES = {
header: 'header',
@@ -15,21 +20,21 @@ const ROW_TYPES = {
export default {
components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownHeader,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownDivider,
+ GlDeprecatedDropdownHeader,
+ GlDeprecatedDropdownItem,
Icon,
},
apollo: {
projectShortPath: {
- query: getProjectShortPath,
+ query: projectShortPathQuery,
},
projectPath: {
- query: getProjectPath,
+ query: projectPathQuery,
},
userPermissions: {
- query: getPermissions,
+ query: permissionsQuery,
variables() {
return {
projectPath: this.projectPath,
@@ -221,11 +226,11 @@ export default {
getComponent(type) {
switch (type) {
case ROW_TYPES.divider:
- return 'gl-dropdown-divider';
+ return 'gl-deprecated-dropdown-divider';
case ROW_TYPES.header:
- return 'gl-dropdown-header';
+ return 'gl-deprecated-dropdown-header';
default:
- return 'gl-dropdown-item';
+ return 'gl-deprecated-dropdown-item';
}
},
},
@@ -241,7 +246,7 @@ export default {
</router-link>
</li>
<li v-if="renderAddToTreeDropdown" class="breadcrumb-item">
- <gl-dropdown toggle-class="add-to-tree qa-add-to-tree ml-1">
+ <gl-deprecated-dropdown toggle-class="add-to-tree qa-add-to-tree ml-1">
<template #button-content>
<span class="sr-only">{{ __('Add to tree') }}</span>
<icon name="plus" :size="16" class="float-left" />
@@ -252,7 +257,7 @@ export default {
{{ item.text }}
</component>
</template>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</li>
</ol>
</nav>
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index c5c99d56e2a..3337ce6c6df 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -8,8 +8,8 @@ import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import getRefMixin from '../mixins/get_ref';
-import getProjectPath from '../queries/getProjectPath.query.graphql';
-import pathLastCommit from '../queries/pathLastCommit.query.graphql';
+import projectPathQuery from '../queries/project_path.query.graphql';
+import pathLastCommitQuery from '../queries/path_last_commit.query.graphql';
export default {
components: {
@@ -28,10 +28,10 @@ export default {
mixins: [getRefMixin],
apollo: {
projectPath: {
- query: getProjectPath,
+ query: projectPathQuery,
},
commit: {
- query: pathLastCommit,
+ query: pathLastCommitQuery,
variables() {
return {
projectPath: this.projectPath,
@@ -102,7 +102,7 @@ export default {
<template v-else-if="commit">
<user-avatar-link
v-if="commit.author"
- :link-href="commit.author.webUrl"
+ :link-href="commit.author.webPath"
:img-src="commit.author.avatarUrl"
:img-size="40"
class="avatar-cell"
@@ -118,13 +118,13 @@ export default {
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<gl-link
- :href="commit.webUrl"
+ :href="commit.webPath"
:class="{ 'font-italic': !commit.message }"
class="commit-row-message item-title"
v-html="commit.titleHtml"
/>
<gl-deprecated-button
- v-if="commit.description"
+ v-if="commit.descriptionHtml"
:class="{ open: showDescription }"
:aria-label="__('Show commit description')"
class="text-expander"
@@ -135,7 +135,7 @@ export default {
<div class="committer">
<gl-link
v-if="commit.author"
- :href="commit.author.webUrl"
+ :href="commit.author.webPath"
class="commit-author-link js-user-link"
>
{{ commit.author.name }}
@@ -147,11 +147,11 @@ export default {
<timeago-tooltip :time="commit.authoredDate" tooltip-placement="bottom" />
</div>
<pre
- v-if="commit.description"
+ v-if="commit.descriptionHtml"
:class="{ 'd-block': showDescription }"
class="commit-row-description gl-mb-3"
- >{{ commit.description }}</pre
- >
+ v-html="commit.descriptionHtml"
+ ></pre>
</div>
<div class="commit-actions flex-row">
<div v-if="commit.signatureHtml" v-html="commit.signatureHtml"></div>
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index f96523bb497..013092ffefd 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -3,15 +3,15 @@ import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { GlLink, GlLoadingIcon } from '@gitlab/ui';
import { handleLocationHash } from '~/lib/utils/common_utils';
-import getReadmeQuery from '../../queries/getReadme.query.graphql';
+import readmeQuery from '../../queries/readme.query.graphql';
export default {
apollo: {
readme: {
- query: getReadmeQuery,
+ query: readmeQuery,
variables() {
return {
- url: this.blob.webUrl,
+ url: this.blob.webPath,
};
},
loadingKey: 'loading',
@@ -51,7 +51,7 @@ export default {
<div class="js-file-title file-title-flex-parent">
<div class="file-header-content">
<i aria-hidden="true" class="fa fa-file-text-o fa-fw"></i>
- <gl-link :href="blob.webUrl">
+ <gl-link :href="blob.webPath">
<strong>{{ blob.name }}</strong>
</gl-link>
</div>
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 5e0ad7acdfd..d0cc617d755 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -2,7 +2,7 @@
import { GlSkeletonLoading } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
import getRefMixin from '../../mixins/get_ref';
-import getProjectPath from '../../queries/getProjectPath.query.graphql';
+import projectPathQuery from '../../queries/project_path.query.graphql';
import TableHeader from './header.vue';
import TableRow from './row.vue';
import ParentRow from './parent_row.vue';
@@ -17,7 +17,7 @@ export default {
mixins: [getRefMixin],
apollo: {
projectPath: {
- query: getProjectPath,
+ query: projectPathQuery,
},
},
props: {
@@ -96,7 +96,7 @@ export default {
:name="entry.name"
:path="entry.flatPath"
:type="entry.type"
- :url="entry.webUrl"
+ :url="entry.webUrl || entry.webPath"
:mode="entry.mode"
:submodule-tree-url="entry.treeUrl"
:lfs-oid="entry.lfsOid"
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 615e329f415..d2fef6693e2 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -12,7 +12,7 @@ import { escapeFileUrl } from '~/lib/utils/url_utility';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import getRefMixin from '../../mixins/get_ref';
-import getCommit from '../../queries/getCommit.query.graphql';
+import commitQuery from '../../queries/commit.query.graphql';
export default {
components: {
@@ -29,7 +29,7 @@ export default {
},
apollo: {
commit: {
- query: getCommit,
+ query: commitQuery,
variables() {
return {
fileName: this.name,
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 59ba1caa8c9..fe3065a2145 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -1,24 +1,28 @@
<script>
-import createFlash from '~/flash';
+import { GlButton } from '@gitlab/ui';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '../../locale';
import FileTable from './table/index.vue';
import getRefMixin from '../mixins/get_ref';
-import getFiles from '../queries/getFiles.query.graphql';
-import getProjectPath from '../queries/getProjectPath.query.graphql';
+import filesQuery from '../queries/files.query.graphql';
+import projectPathQuery from '../queries/project_path.query.graphql';
import FilePreview from './preview/index.vue';
import { readmeFile } from '../utils/readme';
+const LIMIT = 1000;
const PAGE_SIZE = 100;
+export const INITIAL_FETCH_COUNT = LIMIT / PAGE_SIZE;
export default {
components: {
FileTable,
FilePreview,
+ GlButton,
},
mixins: [getRefMixin],
apollo: {
projectPath: {
- query: getProjectPath,
+ query: projectPathQuery,
},
},
props: {
@@ -43,12 +47,19 @@ export default {
blobs: [],
},
isLoadingFiles: false,
+ isOverLimit: false,
+ clickedShowMore: false,
+ pageSize: PAGE_SIZE,
+ fetchCounter: 0,
};
},
computed: {
readme() {
return readmeFile(this.entries.blobs);
},
+ hasShowMore() {
+ return !this.clickedShowMore && this.fetchCounter === INITIAL_FETCH_COUNT;
+ },
},
watch: {
@@ -70,13 +81,13 @@ export default {
return this.$apollo
.query({
- query: getFiles,
+ query: filesQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,
path: this.path || '/',
nextPageCursor: this.nextPageCursor,
- pageSize: PAGE_SIZE,
+ pageSize: this.pageSize,
},
})
.then(({ data }) => {
@@ -96,7 +107,11 @@ export default {
if (pageInfo?.hasNextPage) {
this.nextPageCursor = pageInfo.endCursor;
- this.fetchFiles();
+ this.fetchCounter += 1;
+ if (this.fetchCounter < INITIAL_FETCH_COUNT || this.clickedShowMore) {
+ this.fetchFiles();
+ this.clickedShowMore = false;
+ }
}
})
.catch(error => {
@@ -112,6 +127,10 @@ export default {
.concat(data.trees.pageInfo, data.submodules.pageInfo, data.blobs.pageInfo)
.find(({ hasNextPage }) => hasNextPage);
},
+ showMore() {
+ this.clickedShowMore = true;
+ this.fetchFiles();
+ },
},
};
</script>
@@ -124,6 +143,19 @@ export default {
:is-loading="isLoadingFiles"
:loading-path="loadingPath"
/>
+ <div
+ v-if="hasShowMore"
+ class="gl-border-1 gl-border-gray-100 gl-rounded-base gl-border-t-none gl-border-b-solid gl-border-l-solid gl-border-r-solid gl-rounded-top-right-none gl-rounded-top-left-none gl-mt-n1"
+ >
+ <gl-button
+ variant="link"
+ class="gl-display-flex gl-w-full gl-py-4!"
+ :loading="isLoadingFiles"
+ @click="showMore"
+ >
+ {{ s__('ProjectFileTree|Show more') }}
+ </gl-button>
+ </div>
<file-preview v-if="readme" :blob="readme" />
</div>
</template>
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 4f80ab4ff5d..187bbfed125 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { escapeFileUrl } from '../lib/utils/url_utility';
import createRouter from './router';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
@@ -109,7 +110,7 @@ export default function setupVueRepositoryList() {
return h(TreeActionLink, {
props: {
path: `${historyLink}/${
- this.$route.params.path ? encodeURIComponent(this.$route.params.path) : ''
+ this.$route.params.path ? escapeFileUrl(this.$route.params.path) : ''
}`,
text: __('History'),
},
diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js
index cef17bf7acb..704dd88aabe 100644
--- a/app/assets/javascripts/repository/log_tree.js
+++ b/app/assets/javascripts/repository/log_tree.js
@@ -1,8 +1,8 @@
import { normalizeData } from 'ee_else_ce/repository/utils/commit';
import axios from '~/lib/utils/axios_utils';
-import getCommits from './queries/getCommits.query.graphql';
-import getProjectPath from './queries/getProjectPath.query.graphql';
-import getRef from './queries/getRef.query.graphql';
+import commitsQuery from './queries/commits.query.graphql';
+import projectPathQuery from './queries/project_path.query.graphql';
+import refQuery from './queries/ref.query.graphql';
let fetchpromise;
let resolvers = [];
@@ -22,8 +22,8 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
if (fetchpromise) return fetchpromise;
- const { projectPath } = client.readQuery({ query: getProjectPath });
- const { escapedRef } = client.readQuery({ query: getRef });
+ const { projectPath } = client.readQuery({ query: projectPathQuery });
+ const { escapedRef } = client.readQuery({ query: refQuery });
fetchpromise = axios
.get(
@@ -36,10 +36,10 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
)
.then(({ data, headers }) => {
const headerLogsOffset = headers['more-logs-offset'];
- const { commits } = client.readQuery({ query: getCommits });
+ const { commits } = client.readQuery({ query: commitsQuery });
const newCommitData = [...commits, ...normalizeData(data, path)];
client.writeQuery({
- query: getCommits,
+ query: commitsQuery,
data: { commits: newCommitData },
});
diff --git a/app/assets/javascripts/repository/mixins/get_ref.js b/app/assets/javascripts/repository/mixins/get_ref.js
index 99d19b77c35..1f1880a48c7 100644
--- a/app/assets/javascripts/repository/mixins/get_ref.js
+++ b/app/assets/javascripts/repository/mixins/get_ref.js
@@ -1,9 +1,9 @@
-import getRef from '../queries/getRef.query.graphql';
+import refQuery from '../queries/ref.query.graphql';
export default {
apollo: {
ref: {
- query: getRef,
+ query: refQuery,
manual: true,
result({ data, loading }) {
if (!loading) {
diff --git a/app/assets/javascripts/repository/mixins/preload.js b/app/assets/javascripts/repository/mixins/preload.js
index cb6c2294679..cb1d7f3aac9 100644
--- a/app/assets/javascripts/repository/mixins/preload.js
+++ b/app/assets/javascripts/repository/mixins/preload.js
@@ -1,12 +1,12 @@
-import getFiles from '../queries/getFiles.query.graphql';
+import filesQuery from '../queries/files.query.graphql';
import getRefMixin from './get_ref';
-import getProjectPath from '../queries/getProjectPath.query.graphql';
+import projectPathQuery from '../queries/project_path.query.graphql';
export default {
mixins: [getRefMixin],
apollo: {
projectPath: {
- query: getProjectPath,
+ query: projectPathQuery,
},
},
data() {
@@ -21,7 +21,7 @@ export default {
return this.$apollo
.query({
- query: getFiles,
+ query: filesQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,
diff --git a/app/assets/javascripts/repository/queries/getCommit.query.graphql b/app/assets/javascripts/repository/queries/commit.query.graphql
index e4aeaaff8fe..e4aeaaff8fe 100644
--- a/app/assets/javascripts/repository/queries/getCommit.query.graphql
+++ b/app/assets/javascripts/repository/queries/commit.query.graphql
diff --git a/app/assets/javascripts/repository/queries/getCommits.query.graphql b/app/assets/javascripts/repository/queries/commits.query.graphql
index 0976b8f32d7..0976b8f32d7 100644
--- a/app/assets/javascripts/repository/queries/getCommits.query.graphql
+++ b/app/assets/javascripts/repository/queries/commits.query.graphql
diff --git a/app/assets/javascripts/repository/queries/getFiles.query.graphql b/app/assets/javascripts/repository/queries/files.query.graphql
index feb89df0492..9e9f5303dd4 100644
--- a/app/assets/javascripts/repository/queries/getFiles.query.graphql
+++ b/app/assets/javascripts/repository/queries/files.query.graphql
@@ -22,7 +22,7 @@ query getFiles(
edges {
node {
...TreeEntry
- webUrl
+ webPath
}
}
pageInfo {
@@ -46,7 +46,7 @@ query getFiles(
node {
...TreeEntry
mode
- webUrl
+ webPath
lfsOid
}
}
diff --git a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql b/app/assets/javascripts/repository/queries/path_last_commit.query.graphql
index f54f09fd647..51f3f790a5d 100644
--- a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
+++ b/app/assets/javascripts/repository/queries/path_last_commit.query.graphql
@@ -6,16 +6,16 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
sha
title
titleHtml
- description
+ descriptionHtml
message
- webUrl
+ webPath
authoredDate
authorName
authorGravatar
author {
name
avatarUrl
- webUrl
+ webPath
}
signatureHtml
pipelines(ref: $ref, first: 1) {
diff --git a/app/assets/javascripts/repository/queries/getPermissions.query.graphql b/app/assets/javascripts/repository/queries/permissions.query.graphql
index 092fa44e2d0..092fa44e2d0 100644
--- a/app/assets/javascripts/repository/queries/getPermissions.query.graphql
+++ b/app/assets/javascripts/repository/queries/permissions.query.graphql
diff --git a/app/assets/javascripts/repository/queries/getProjectPath.query.graphql b/app/assets/javascripts/repository/queries/project_path.query.graphql
index 74e73e07577..74e73e07577 100644
--- a/app/assets/javascripts/repository/queries/getProjectPath.query.graphql
+++ b/app/assets/javascripts/repository/queries/project_path.query.graphql
diff --git a/app/assets/javascripts/repository/queries/getProjectShortPath.query.graphql b/app/assets/javascripts/repository/queries/project_short_path.query.graphql
index 34eb26598c2..34eb26598c2 100644
--- a/app/assets/javascripts/repository/queries/getProjectShortPath.query.graphql
+++ b/app/assets/javascripts/repository/queries/project_short_path.query.graphql
diff --git a/app/assets/javascripts/repository/queries/getReadme.query.graphql b/app/assets/javascripts/repository/queries/readme.query.graphql
index cf056330133..cf056330133 100644
--- a/app/assets/javascripts/repository/queries/getReadme.query.graphql
+++ b/app/assets/javascripts/repository/queries/readme.query.graphql
diff --git a/app/assets/javascripts/repository/queries/getRef.query.graphql b/app/assets/javascripts/repository/queries/ref.query.graphql
index 91afb751626..91afb751626 100644
--- a/app/assets/javascripts/repository/queries/getRef.query.graphql
+++ b/app/assets/javascripts/repository/queries/ref.query.graphql
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 0bb33de0234..8bebd16ace7 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
-import flash from './flash';
+import { deprecatedCreateFlash as flash } from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 05e0b9e7089..990c8faf253 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -12,6 +12,7 @@ import {
getProjectSlug,
spriteIcon,
} from './lib/utils/common_utils';
+import Tracking from '~/tracking';
/**
* Search input in top navigation bar.
@@ -355,6 +356,15 @@ export class SearchAutocomplete {
if (!this.dropdown.hasClass('show')) {
this.loadingSuggestions = false;
this.dropdownToggle.dropdown('toggle');
+
+ const trackEvent = 'click_search_bar';
+ const trackCategory = undefined; // will be default set in event method
+
+ Tracking.event(trackCategory, trackEvent, {
+ label: 'main_navigation',
+ property: 'navigation',
+ });
+
return this.searchInput.removeClass('js-autocomplete-disabled');
}
}
diff --git a/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue b/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue
index 807f10bd9c6..1530e9a15b5 100644
--- a/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue
+++ b/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue
@@ -1,7 +1,7 @@
<script>
-import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
+import Stacktrace from '~/error_tracking/components/stacktrace.vue';
export default {
name: 'SentryErrorStackTrace',
diff --git a/app/assets/javascripts/serverless/components/empty_state.vue b/app/assets/javascripts/serverless/components/empty_state.vue
index 2683805f2f7..49922ad8e6c 100644
--- a/app/assets/javascripts/serverless/components/empty_state.vue
+++ b/app/assets/javascripts/serverless/components/empty_state.vue
@@ -1,40 +1,38 @@
<script>
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { mapState } from 'vuex';
+
export default {
- props: {
- clustersPath: {
- type: String,
- required: true,
- },
- helpPath: {
- type: String,
- required: true,
- },
+ components: {
+ GlEmptyState,
+ GlLink,
+ GlSprintf,
+ },
+ computed: {
+ ...mapState(['clustersPath', 'emptyImagePath', 'helpPath']),
},
};
</script>
<template>
- <div class="row empty-state js-empty-state">
- <div class="col-12">
- <div class="text-content">
- <h4 class="state-title text-center">
- {{ s__('Serverless|Getting started with serverless') }}
- </h4>
- <p class="state-description">
- {{
- s__(`Serverless| In order to start using functions as a service,
- you must first install Knative on your Kubernetes cluster.`)
- }}
-
- <a :href="helpPath"> {{ __('More information') }} </a>
- </p>
-
- <div class="text-center">
- <a :href="clustersPath" class="btn btn-success">
- {{ s__('Serverless|Install Knative') }}
- </a>
- </div>
- </div>
- </div>
- </div>
+ <gl-empty-state
+ :svg-path="emptyImagePath"
+ :title="s__('Serverless|Getting started with serverless')"
+ :primary-button-link="clustersPath"
+ :primary-button-text="s__('Serverless|Install Knative')"
+ >
+ <template #description>
+ <gl-sprintf
+ :message="
+ s__(
+ 'Serverless|In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. %{linkStart}More information%{linkEnd}',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="helpPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
</template>
diff --git a/app/assets/javascripts/serverless/components/function_details.vue b/app/assets/javascripts/serverless/components/function_details.vue
index 53c78b93254..6ab44eec5cd 100644
--- a/app/assets/javascripts/serverless/components/function_details.vue
+++ b/app/assets/javascripts/serverless/components/function_details.vue
@@ -23,14 +23,6 @@ export default {
required: false,
default: false,
},
- clustersPath: {
- type: String,
- required: true,
- },
- helpPath: {
- type: String,
- required: true,
- },
},
data() {
return {
@@ -96,8 +88,6 @@ export default {
<area-chart v-if="hasPrometheusData" :graph-data="graphData" :container-width="elWidth" />
<missing-prometheus
v-if="!hasPrometheus || hasPrometheusMissingData"
- :help-path="helpPath"
- :clusters-path="clustersPath"
:missing-data="hasPrometheusMissingData"
/>
</section>
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index 8fa48134f1f..c44a14f1785 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLink, GlLoadingIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
@@ -10,24 +10,11 @@ export default {
components: {
EnvironmentRow,
EmptyState,
+ GlLink,
GlLoadingIcon,
},
- props: {
- clustersPath: {
- type: String,
- required: true,
- },
- helpPath: {
- type: String,
- required: true,
- },
- statusPath: {
- type: String,
- required: true,
- },
- },
computed: {
- ...mapState(['installed', 'isLoading', 'hasFunctionData']),
+ ...mapState(['installed', 'isLoading', 'hasFunctionData', 'helpPath', 'statusPath']),
...mapGetters(['getFunctions']),
checkingInstalled() {
@@ -118,14 +105,14 @@ export default {
}}
</p>
<div class="text-center">
- <a :href="helpPath" class="btn btn-success">
- {{ s__('Serverless|Learn more about Serverless') }}
- </a>
+ <gl-link :href="helpPath" class="btn btn-success">{{
+ s__('Serverless|Learn more about Serverless')
+ }}</gl-link>
</div>
</div>
</div>
</div>
- <empty-state v-else :clusters-path="clustersPath" :help-path="helpPath" />
+ <empty-state v-else />
</section>
</template>
diff --git a/app/assets/javascripts/serverless/components/missing_prometheus.vue b/app/assets/javascripts/serverless/components/missing_prometheus.vue
index 6c29f7c89ff..0d2c9f5151c 100644
--- a/app/assets/javascripts/serverless/components/missing_prometheus.vue
+++ b/app/assets/javascripts/serverless/components/missing_prometheus.vue
@@ -1,5 +1,6 @@
<script>
import { GlDeprecatedButton, GlLink } from '@gitlab/ui';
+import { mapState } from 'vuex';
import { s__ } from '../../locale';
export default {
@@ -8,20 +9,13 @@ export default {
GlLink,
},
props: {
- clustersPath: {
- type: String,
- required: true,
- },
- helpPath: {
- type: String,
- required: true,
- },
missingData: {
type: Boolean,
required: true,
},
},
computed: {
+ ...mapState(['clustersPath', 'helpPath']),
missingStateClass() {
return this.missingData ? 'missing-prometheus-state' : 'empty-prometheus-state';
},
diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js
index ed3b633d766..24a9b4ac521 100644
--- a/app/assets/javascripts/serverless/serverless_bundle.js
+++ b/app/assets/javascripts/serverless/serverless_bundle.js
@@ -6,6 +6,9 @@ import { createStore } from './store';
export default class Serverless {
constructor() {
if (document.querySelector('.js-serverless-function-details-page') != null) {
+ const entryPointData = document.querySelector('.js-serverless-function-details-page').dataset;
+ const store = createStore(entryPointData);
+
const {
serviceName,
serviceDescription,
@@ -15,9 +18,7 @@ export default class Serverless {
servicePodcount,
serviceMetricsUrl,
prometheus,
- clustersPath,
- helpPath,
- } = document.querySelector('.js-serverless-function-details-page').dataset;
+ } = entryPointData;
const el = document.querySelector('#js-serverless-function-details');
const service = {
@@ -32,35 +33,26 @@ export default class Serverless {
this.functionDetails = new Vue({
el,
- store: createStore(),
+ store,
render(createElement) {
return createElement(FunctionDetails, {
props: {
func: service,
hasPrometheus: prometheus !== undefined,
- clustersPath,
- helpPath,
},
});
},
});
} else {
- const { statusPath, clustersPath, helpPath } = document.querySelector(
- '.js-serverless-functions-page',
- ).dataset;
+ const entryPointData = document.querySelector('.js-serverless-functions-page').dataset;
+ const store = createStore(entryPointData);
const el = document.querySelector('#js-serverless-functions');
this.functions = new Vue({
el,
- store: createStore(),
+ store,
render(createElement) {
- return createElement(Functions, {
- props: {
- clustersPath,
- helpPath,
- statusPath,
- },
- });
+ return createElement(Functions);
},
});
}
diff --git a/app/assets/javascripts/serverless/store/actions.js b/app/assets/javascripts/serverless/store/actions.js
index a0a9fdf7ace..b9d57138efa 100644
--- a/app/assets/javascripts/serverless/store/actions.js
+++ b/app/assets/javascripts/serverless/store/actions.js
@@ -2,7 +2,7 @@ import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import { MAX_REQUESTS, CHECKING_INSTALLED, TIMEOUT } from '../constants';
@@ -123,6 +123,3 @@ export const fetchMetrics = ({ dispatch }, { metricsPath, hasPrometheus }) => {
createFlash(error);
});
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/serverless/store/getters.js b/app/assets/javascripts/serverless/store/getters.js
index 071f663d9d2..c9b1d22799a 100644
--- a/app/assets/javascripts/serverless/store/getters.js
+++ b/app/assets/javascripts/serverless/store/getters.js
@@ -5,6 +5,3 @@ export const hasPrometheusMissingData = state => state.hasPrometheus && !state.h
// Convert the function list into a k/v grouping based on the environment scope
export const getFunctions = state => translate(state.functions);
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/serverless/store/index.js b/app/assets/javascripts/serverless/store/index.js
index 5f72060633e..6f32d85201e 100644
--- a/app/assets/javascripts/serverless/store/index.js
+++ b/app/assets/javascripts/serverless/store/index.js
@@ -7,12 +7,12 @@ import createState from './state';
Vue.use(Vuex);
-export const createStore = () =>
+export const createStore = (entryPointData = {}) =>
new Vuex.Store({
actions,
getters,
mutations,
- state: createState(),
+ state: createState(entryPointData),
});
-export default createStore();
+export default createStore;
diff --git a/app/assets/javascripts/serverless/store/state.js b/app/assets/javascripts/serverless/store/state.js
index fdd29299749..353bfcf3fed 100644
--- a/app/assets/javascripts/serverless/store/state.js
+++ b/app/assets/javascripts/serverless/store/state.js
@@ -1,14 +1,22 @@
-export default () => ({
+export default (
+ initialState = { clustersPath: null, helpPath: null, emptyImagePath: null, statusPath: null },
+) => ({
+ clustersPath: initialState.clustersPath,
error: null,
+ helpPath: initialState.helpPath,
installed: 'checking',
isLoading: true,
// functions
functions: [],
hasFunctionData: true,
+ statusPath: initialState.statusPath,
// function_details
hasPrometheus: true,
hasPrometheusData: false,
graphData: {},
+
+ // empty_state
+ emptyImagePath: initialState.emptyImagePath,
});
diff --git a/app/assets/javascripts/serverless/survey_banner.vue b/app/assets/javascripts/serverless/survey_banner.vue
index a0a90fa5e80..18ab8315840 100644
--- a/app/assets/javascripts/serverless/survey_banner.vue
+++ b/app/assets/javascripts/serverless/survey_banner.vue
@@ -1,7 +1,7 @@
<script>
import Cookies from 'js-cookie';
-import { parseBoolean } from '~/lib/utils/common_utils';
import { GlBanner } from '@gitlab/ui';
+import { parseBoolean } from '~/lib/utils/common_utils';
export default {
components: {
diff --git a/app/assets/javascripts/serverless/utils.js b/app/assets/javascripts/serverless/utils.js
index 8b9e96ce9aa..1bf03ea8d42 100644
--- a/app/assets/javascripts/serverless/utils.js
+++ b/app/assets/javascripts/serverless/utils.js
@@ -18,6 +18,3 @@ export const translate = functions =>
}),
{},
);
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
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 d5ae9b04090..cb047530c17 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -2,7 +2,7 @@
import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { GlModal, GlTooltipDirective } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import { __, s__ } from '~/locale';
import Api from '~/api';
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 0906d5abec3..14c14d0bad1 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,5 +1,5 @@
<script>
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 0987603cafd..c6f7d5e44ad 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,12 +1,10 @@
<script>
-import { mapState, mapActions } from 'vuex';
-import { __ } from '~/locale';
-import Flash from '~/flash';
+import { mapState } from 'vuex';
+import { __, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import EditForm from './edit_form.vue';
-import recaptchaModalImplementor from '~/vue_shared/mixins/recaptcha_modal_implementor';
export default {
components: {
@@ -16,7 +14,6 @@ export default {
directives: {
tooltip,
},
- mixins: [recaptchaModalImplementor],
props: {
fullPath: {
required: true,
@@ -26,9 +23,10 @@ export default {
required: true,
type: Boolean,
},
- service: {
- required: true,
- type: Object,
+ issuableType: {
+ required: false,
+ type: String,
+ default: 'issue',
},
},
data() {
@@ -37,45 +35,36 @@ export default {
};
},
computed: {
- ...mapState({ confidential: ({ noteableData }) => noteableData.confidential }),
+ ...mapState({
+ confidential: ({ noteableData, confidential }) => {
+ if (noteableData) {
+ return noteableData.confidential;
+ }
+ return Boolean(confidential);
+ },
+ }),
confidentialityIcon() {
return this.confidential ? 'eye-slash' : 'eye';
},
tooltipLabel() {
return this.confidential ? __('Confidential') : __('Not confidential');
},
+ confidentialText() {
+ return sprintf(__('This %{issuableType} is confidential'), {
+ issuableType: this.issuableType,
+ });
+ },
},
created() {
- eventHub.$on('updateConfidentialAttribute', this.updateConfidentialAttribute);
eventHub.$on('closeConfidentialityForm', this.toggleForm);
},
beforeDestroy() {
- eventHub.$off('updateConfidentialAttribute', this.updateConfidentialAttribute);
eventHub.$off('closeConfidentialityForm', this.toggleForm);
},
methods: {
- ...mapActions(['setConfidentiality']),
toggleForm() {
this.edit = !this.edit;
},
- closeForm() {
- this.edit = false;
- },
- updateConfidentialAttribute() {
- // TODO: rm when FF is defaulted to on.
- const confidential = !this.confidential;
- this.service
- .update('issue', { confidential })
- .then(({ data }) => this.checkForSpam(data))
- .then(() => window.location.reload())
- .catch(error => {
- if (error.name === 'SpamError') {
- this.openRecaptcha();
- } else {
- Flash(__('Something went wrong trying to change the confidentiality of this issue'));
- }
- });
- },
},
};
</script>
@@ -109,7 +98,12 @@ export default {
>
</div>
<div class="value sidebar-item-value hide-collapsed">
- <edit-form v-if="edit" :is-confidential="confidential" :full-path="fullPath" />
+ <edit-form
+ v-if="edit"
+ :confidential="confidential"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ />
<div v-if="!confidential" class="no-value sidebar-item-value" data-testid="not-confidential">
<icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" />
{{ __('Not confidential') }}
@@ -121,10 +115,8 @@ export default {
aria-hidden="true"
class="sidebar-item-icon inline is-active"
/>
- {{ __('This issue is confidential') }}
+ {{ confidentialText }}
</div>
</div>
-
- <recaptcha-modal v-if="showRecaptcha" :html="recaptchaHTML" @close="closeRecaptcha" />
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index 9dd4f04acdb..17e44cf0e1d 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -1,13 +1,15 @@
<script>
+import { GlSprintf } from '@gitlab/ui';
import editFormButtons from './edit_form_buttons.vue';
-import { s__ } from '../../../locale';
+import { __ } from '../../../locale';
export default {
components: {
editFormButtons,
+ GlSprintf,
},
props: {
- isConfidential: {
+ confidential: {
required: true,
type: Boolean,
},
@@ -15,16 +17,20 @@ export default {
required: true,
type: String,
},
+ issuableType: {
+ required: true,
+ type: String,
+ },
},
computed: {
confidentialityOnWarning() {
- return s__(
- 'confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.',
+ return __(
+ 'You are going to turn on the confidentiality. This means that only team members with %{strongStart}at least Reporter access%{strongEnd} are able to see and leave comments on the %{issuableType}.',
);
},
confidentialityOffWarning() {
- return s__(
- 'confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.',
+ return __(
+ 'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}.',
);
},
},
@@ -35,9 +41,23 @@ export default {
<div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message">
<div>
- <p v-if="!isConfidential" v-html="confidentialityOnWarning"></p>
- <p v-else v-html="confidentialityOffWarning"></p>
- <edit-form-buttons :full-path="fullPath" />
+ <p v-if="!confidential">
+ <gl-sprintf :message="confidentialityOnWarning">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #issuableType>{{ issuableType }}</template>
+ </gl-sprintf>
+ </p>
+ <p v-else>
+ <gl-sprintf :message="confidentialityOffWarning">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #issuableType>{{ issuableType }}</template>
+ </gl-sprintf>
+ </p>
+ <edit-form-buttons :full-path="fullPath" :confidential="confidential" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 80928649a03..86bfacbfb9e 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -1,22 +1,24 @@
<script>
import $ from 'jquery';
import { GlLoadingIcon } from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
+import { mapActions } from 'vuex';
import { __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import eventHub from '../../event_hub';
export default {
components: {
GlLoadingIcon,
},
- mixins: [glFeatureFlagsMixin()],
props: {
fullPath: {
required: true,
type: String,
},
+ confidential: {
+ required: true,
+ type: Boolean,
+ },
},
data() {
return {
@@ -24,7 +26,6 @@ export default {
};
},
computed: {
- ...mapState({ confidential: ({ noteableData }) => noteableData.confidential }),
toggleButtonText() {
if (this.isLoading) {
return __('Applying');
@@ -34,7 +35,7 @@ export default {
},
},
methods: {
- ...mapActions(['updateConfidentialityOnIssue']),
+ ...mapActions(['updateConfidentialityOnIssuable']),
closeForm() {
eventHub.$emit('closeConfidentialityForm');
$(this.$el).trigger('hidden.gl.dropdown');
@@ -43,18 +44,19 @@ export default {
this.isLoading = true;
const confidential = !this.confidential;
- if (this.glFeatures.confidentialApolloSidebar) {
- this.updateConfidentialityOnIssue({ confidential, fullPath: this.fullPath })
- .catch(() => {
- Flash(__('Something went wrong trying to change the confidentiality of this issue'));
- })
- .finally(() => {
- this.closeForm();
- this.isLoading = false;
- });
- } else {
- eventHub.$emit('updateConfidentialAttribute');
- }
+ this.updateConfidentialityOnIssuable({ confidential, fullPath: this.fullPath })
+ .then(() => {
+ eventHub.$emit('updateIssuableConfidentiality', confidential);
+ })
+ .catch(err => {
+ Flash(
+ err || __('Something went wrong trying to change the confidentiality of this issue'),
+ );
+ })
+ .finally(() => {
+ this.closeForm();
+ this.isLoading = false;
+ });
},
},
};
diff --git a/app/assets/javascripts/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql b/app/assets/javascripts/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql
index 2459aa346c9..5caf5f6b555 100644
--- a/app/assets/javascripts/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql
+++ b/app/assets/javascripts/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql
@@ -3,5 +3,6 @@ mutation updateIssueConfidential($input: IssueSetConfidentialInput!) {
issue {
confidential
}
+ errors
}
}
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index 630da751704..65b51169420 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -1,40 +1,20 @@
<script>
+import { GlSprintf } from '@gitlab/ui';
import editFormButtons from './edit_form_buttons.vue';
-import issuableMixin from '../../../vue_shared/mixins/issuable';
-import { __, sprintf } from '../../../locale';
export default {
components: {
editFormButtons,
+ GlSprintf,
},
- mixins: [issuableMixin],
props: {
isLocked: {
required: true,
type: Boolean,
},
-
- updateLockedAttribute: {
+ issuableDisplayName: {
required: true,
- type: Function,
- },
- },
- computed: {
- lockWarning() {
- return sprintf(
- __(
- 'Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.',
- ),
- { issuableDisplayName: this.issuableDisplayName },
- );
- },
- unlockWarning() {
- return sprintf(
- __(
- 'Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.',
- ),
- { issuableDisplayName: this.issuableDisplayName },
- );
+ type: String,
},
},
};
@@ -42,12 +22,38 @@ export default {
<template>
<div class="dropdown show">
- <div class="dropdown-menu sidebar-item-warning-message">
- <p v-if="isLocked" class="text" v-html="unlockWarning"></p>
+ <div class="dropdown-menu sidebar-item-warning-message" data-testid="warning-text">
+ <p v-if="isLocked" class="text">
+ <gl-sprintf
+ :message="
+ __(
+ 'Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment.',
+ )
+ "
+ >
+ <template #issuableDisplayName>{{ issuableDisplayName }}</template>
+ <template #strong="{ content }"
+ ><strong>{{ content }}</strong></template
+ >
+ </gl-sprintf>
+ </p>
- <p v-else class="text" v-html="lockWarning"></p>
+ <p v-else class="text">
+ <gl-sprintf
+ :message="
+ __(
+ 'Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment.',
+ )
+ "
+ >
+ <template #issuableDisplayName>{{ issuableDisplayName }}</template>
+ <template #strong="{ content }"
+ ><strong>{{ content }}</strong></template
+ >
+ </gl-sprintf>
+ </p>
- <edit-form-buttons :is-locked="isLocked" :update-locked-attribute="updateLockedAttribute" />
+ <edit-form-buttons :is-locked="isLocked" :issuable-display-name="issuableDisplayName" />
</div>
</div>
</template>
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 2e85ded8ade..ea7230ae488 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,39 +1,63 @@
<script>
import $ from 'jquery';
-import { __ } from '~/locale';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mapActions } from 'vuex';
+import { __, sprintf } from '../../../locale';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import eventHub from '../../event_hub';
export default {
+ components: {
+ GlLoadingIcon,
+ },
+ inject: ['fullPath'],
props: {
isLocked: {
required: true,
type: Boolean,
},
-
- updateLockedAttribute: {
+ issuableDisplayName: {
required: true,
- type: Function,
+ type: String,
},
},
-
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
computed: {
buttonText() {
- return this.isLocked ? __('Unlock') : __('Lock');
- },
+ if (this.isLoading) {
+ return __('Applying');
+ }
- toggleLock() {
- return !this.isLocked;
+ return this.isLocked ? __('Unlock') : __('Lock');
},
},
-
methods: {
+ ...mapActions(['updateLockedAttribute']),
closeForm() {
eventHub.$emit('closeLockForm');
$(this.$el).trigger('hidden.gl.dropdown');
},
submitForm() {
- this.closeForm();
- this.updateLockedAttribute(this.toggleLock);
+ this.isLoading = true;
+
+ this.updateLockedAttribute({
+ locked: !this.isLocked,
+ fullPath: this.fullPath,
+ })
+ .catch(() => {
+ const flashMessage = __(
+ 'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
+ );
+ Flash(sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }));
+ })
+ .finally(() => {
+ this.closeForm();
+ this.isLoading = false;
+ });
},
},
};
@@ -45,7 +69,14 @@ export default {
{{ __('Cancel') }}
</button>
- <button type="button" class="btn btn-close" @click.prevent="submitForm">
+ <button
+ type="button"
+ data-testid="lock-toggle"
+ class="btn btn-close"
+ :disabled="isLoading"
+ @click.prevent="submitForm"
+ >
+ <gl-loading-icon v-if="isLoading" inline />
{{ buttonText }}
</button>
</div>
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
new file mode 100644
index 00000000000..1b4968fabf6
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -0,0 +1,129 @@
+<script>
+import { mapGetters } from 'vuex';
+import { __ } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+import Icon from '~/vue_shared/components/icon.vue';
+import eventHub from '~/sidebar/event_hub';
+import editForm from './edit_form.vue';
+
+export default {
+ issue: 'issue',
+ locked: {
+ icon: 'lock',
+ class: 'value',
+ iconClass: 'is-active',
+ displayText: __('Locked'),
+ },
+ unlocked: {
+ class: ['no-value hide-collapsed'],
+ icon: 'lock-open',
+ iconClass: '',
+ displayText: __('Unlocked'),
+ },
+ components: {
+ editForm,
+ Icon,
+ },
+
+ directives: {
+ tooltip,
+ },
+
+ props: {
+ isEditable: {
+ required: true,
+ type: Boolean,
+ },
+ },
+ data() {
+ return {
+ isLockDialogOpen: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['getNoteableData']),
+ issuableDisplayName() {
+ const isInIssuePage = this.getNoteableData.targetType === this.$options.issue;
+ return isInIssuePage ? __('issue') : __('merge request');
+ },
+ isLocked() {
+ return this.getNoteableData.discussion_locked;
+ },
+ lockStatus() {
+ return this.isLocked ? this.$options.locked : this.$options.unlocked;
+ },
+
+ tooltipLabel() {
+ return this.isLocked ? __('Locked') : __('Unlocked');
+ },
+ },
+
+ created() {
+ eventHub.$on('closeLockForm', this.toggleForm);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('closeLockForm', this.toggleForm);
+ },
+
+ methods: {
+ toggleForm() {
+ if (this.isEditable) {
+ this.isLockDialogOpen = !this.isLockDialogOpen;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block issuable-sidebar-item lock">
+ <div
+ v-tooltip
+ :title="tooltipLabel"
+ class="sidebar-collapsed-icon"
+ data-testid="sidebar-collapse-icon"
+ data-container="body"
+ data-placement="left"
+ data-boundary="viewport"
+ @click="toggleForm"
+ >
+ <icon :name="lockStatus.icon" class="sidebar-item-icon is-active" />
+ </div>
+
+ <div class="title hide-collapsed">
+ {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
+ <a
+ v-if="isEditable"
+ class="float-right lock-edit"
+ href="#"
+ data-testid="edit-link"
+ data-track-event="click_edit_button"
+ data-track-label="right_sidebar"
+ data-track-property="lock_issue"
+ @click.prevent="toggleForm"
+ >
+ {{ __('Edit') }}
+ </a>
+ </div>
+
+ <div class="value sidebar-item-value hide-collapsed">
+ <edit-form
+ v-if="isLockDialogOpen"
+ data-testid="edit-form"
+ :is-locked="isLocked"
+ :issuable-display-name="issuableDisplayName"
+ />
+
+ <div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class">
+ <icon
+ :size="16"
+ :name="lockStatus.icon"
+ class="sidebar-item-icon"
+ :class="lockStatus.iconClass"
+ />
+ {{ lockStatus.displayText }}
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
deleted file mode 100644
index 728f655d33d..00000000000
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ /dev/null
@@ -1,140 +0,0 @@
-<script>
-import { __, sprintf } from '~/locale';
-import Flash from '~/flash';
-import tooltip from '~/vue_shared/directives/tooltip';
-import issuableMixin from '~/vue_shared/mixins/issuable';
-import Icon from '~/vue_shared/components/icon.vue';
-import eventHub from '~/sidebar/event_hub';
-import editForm from './edit_form.vue';
-
-export default {
- components: {
- editForm,
- Icon,
- },
-
- directives: {
- tooltip,
- },
-
- mixins: [issuableMixin],
-
- props: {
- isLocked: {
- required: true,
- type: Boolean,
- },
-
- isEditable: {
- required: true,
- type: Boolean,
- },
-
- mediator: {
- required: true,
- type: Object,
- validator(mediatorObject) {
- return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
- },
- },
- },
-
- computed: {
- lockIcon() {
- return this.isLocked ? 'lock' : 'lock-open';
- },
-
- isLockDialogOpen() {
- return this.mediator.store.isLockDialogOpen;
- },
-
- tooltipLabel() {
- return this.isLocked ? __('Locked') : __('Unlocked');
- },
- },
-
- created() {
- eventHub.$on('closeLockForm', this.toggleForm);
- },
-
- beforeDestroy() {
- eventHub.$off('closeLockForm', this.toggleForm);
- },
-
- methods: {
- toggleForm() {
- if (this.isEditable) {
- this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
- }
- },
- updateLockedAttribute(locked) {
- this.mediator.service
- .update(this.issuableType, {
- discussion_locked: locked,
- })
- .then(() => window.location.reload())
- .catch(() =>
- Flash(
- sprintf(
- __(
- 'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
- ),
- {
- issuableDisplayName: this.issuableDisplayName,
- },
- ),
- ),
- );
- },
- },
-};
-</script>
-
-<template>
- <div class="block issuable-sidebar-item lock">
- <div
- v-tooltip
- :title="tooltipLabel"
- class="sidebar-collapsed-icon"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
- @click="toggleForm"
- >
- <icon :name="lockIcon" class="sidebar-item-icon is-active" />
- </div>
-
- <div class="title hide-collapsed">
- {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
- <a
- v-if="isEditable"
- class="float-right lock-edit"
- href="#"
- data-track-event="click_edit_button"
- data-track-label="right_sidebar"
- data-track-property="lock_issue"
- @click.prevent="toggleForm"
- >
- {{ __('Edit') }}
- </a>
- </div>
-
- <div class="value sidebar-item-value hide-collapsed">
- <edit-form
- v-if="isLockDialogOpen"
- :is-locked="isLocked"
- :update-locked-attribute="updateLockedAttribute"
- :issuable-type="issuableType"
- />
-
- <div v-if="isLocked" class="value sidebar-item-value">
- <icon :size="16" name="lock" class="sidebar-item-icon inline is-active" />
- {{ __('Locked') }}
- </div>
-
- <div v-else class="no-value sidebar-item-value hide-collapsed">
- <icon :size="16" name="lock-open" class="sidebar-item-icon inline" /> {{ __('Unlocked') }}
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql b/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql
new file mode 100644
index 00000000000..2a1bcdf7136
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql
@@ -0,0 +1,8 @@
+mutation updateIssueLocked($input: IssueSetLockedInput!) {
+ issueSetLocked(input: $input) {
+ issue {
+ discussionLocked
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql b/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql
new file mode 100644
index 00000000000..8590c8e71a6
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql
@@ -0,0 +1,8 @@
+mutation updateMergeRequestLocked($input: MergeRequestSetLockedInput!) {
+ mergeRequestSetLocked(input: $input) {
+ mergeRequest {
+ discussionLocked
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
index 91fe5fc50a9..ee1c98e9d69 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
@@ -1,6 +1,6 @@
<script>
import Store from '../../stores/sidebar_store';
-import Flash from '../../../flash';
+import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __ } from '../../../locale';
import subscriptions from './subscriptions.vue';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 5cf574e1387..67a8f11b760 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,4 +1,5 @@
<script>
+import { GlIcon } from '@gitlab/ui';
import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
@@ -11,6 +12,7 @@ import eventHub from '../../event_hub';
export default {
name: 'IssuableTimeTracker',
components: {
+ GlIcon,
TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane,
TimeTrackingSpentOnlyPane,
@@ -111,7 +113,7 @@ export default {
class="close-help-button float-right"
@click="toggleHelpState(false)"
>
- <i class="fa fa-close" aria-hidden="true"> </i>
+ <gl-icon name="close" />
</div>
</div>
<div class="time-tracking-content hide-collapsed">
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index 3b7df369237..5281c03ab3f 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -6,7 +6,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
const MARK_TEXT = __('Mark as done');
-const TODO_TEXT = __('Add a To Do');
+const TODO_TEXT = __('Add a To-Do');
export default {
directives: {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 2c108835c36..015219200db 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -5,13 +5,14 @@ import SidebarTimeTracking from './components/time_tracking/sidebar_time_trackin
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue';
-import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
+import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
import { store } from '~/notes/stores';
import { isInIssuePage } from '~/lib/utils/common_utils';
+import mergeRequestStore from '~/mr_notes/stores';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -79,24 +80,28 @@ function mountConfidentialComponent(mediator) {
});
}
-function mountLockComponent(mediator) {
+function mountLockComponent() {
const el = document.getElementById('js-lock-entry-point');
-
- if (!el) return;
+ const { fullPath } = getSidebarOptions();
const dataNode = document.getElementById('js-lock-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
- const LockComp = Vue.extend(LockIssueSidebar);
-
- new LockComp({
- propsData: {
- isLocked: initialData.is_locked,
- isEditable: initialData.is_editable,
- mediator,
- issuableType: isInIssuePage() ? 'issue' : 'merge_request',
- },
- }).$mount(el);
+ return el
+ ? new Vue({
+ el,
+ store: isInIssuePage() ? store : mergeRequestStore,
+ provide: {
+ fullPath,
+ },
+ render: createElement =>
+ createElement(IssuableLockForm, {
+ props: {
+ isEditable: initialData.is_editable,
+ },
+ }),
+ })
+ : undefined;
}
function mountParticipantsComponent(mediator) {
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 3b8903b4a4c..8714bea1729 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -1,7 +1,7 @@
-import axios from '~/lib/utils/axios_utils';
-import createGqClient, { fetchPolicies } from '~/lib/graphql';
import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
import sidebarDetailsForHealthStatusFeatureFlagQuery from 'ee_else_ce/sidebar/queries/sidebarDetailsForHealthStatusFeatureFlag.query.graphql';
+import axios from '~/lib/utils/axios_utils';
+import createGqClient, { fetchPolicies } from '~/lib/graphql';
export const gqClient = createGqClient(
{},
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 34621fc1036..8f1f76a2e02 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,6 +1,6 @@
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import { visitUrl } from '../lib/utils/url_utility';
-import Flash from '../flash';
+import { deprecatedCreateFlash as Flash } from '../flash';
import Service from './services/sidebar_service';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 2d505c4c96b..586d1e62c2f 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
-import createFlash from './flash';
+import { deprecatedCreateFlash as createFlash } from './flash';
import FilesCommentButton from './files_comment_button';
import initImageDiffHelper from './image_diff/helpers/init_image_diff';
import syntaxHighlight from './syntax_highlight';
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index c01f9524ca8..6e3a670dc38 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { __, sprintf } from '~/locale';
import TitleField from '~/vue_shared/components/form/title.vue';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -14,19 +14,17 @@ import {
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_CREATE_MUTATION_ERROR,
SNIPPET_UPDATE_MUTATION_ERROR,
- SNIPPET_BLOB_ACTION_CREATE,
- SNIPPET_BLOB_ACTION_UPDATE,
- SNIPPET_BLOB_ACTION_MOVE,
} from '../constants';
-import SnippetBlobEdit from './snippet_blob_edit.vue';
+import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
import SnippetDescriptionEdit from './snippet_description_edit.vue';
+import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
export default {
components: {
SnippetDescriptionEdit,
SnippetVisibilityEdit,
- SnippetBlobEdit,
+ SnippetBlobActionsEdit,
TitleField,
FormFooterActions,
GlButton,
@@ -55,25 +53,20 @@ export default {
},
data() {
return {
- blobsActions: {},
isUpdating: false,
newSnippet: false,
+ actions: [],
};
},
computed: {
- getActionsEntries() {
- return Object.values(this.blobsActions);
+ hasBlobChanges() {
+ return this.actions.length > 0;
},
- allBlobsHaveContent() {
- const entries = this.getActionsEntries;
- return entries.length > 0 && !entries.find(action => !action.content);
- },
- allBlobChangesRegistered() {
- const entries = this.getActionsEntries;
- return entries.length > 0 && !entries.find(action => action.action === '');
+ hasValidBlobs() {
+ return this.actions.every(x => x.filePath && x.content);
},
updatePrevented() {
- return this.snippet.title === '' || !this.allBlobsHaveContent || this.isUpdating;
+ return this.snippet.title === '' || !this.hasValidBlobs || this.isUpdating;
},
isProjectSnippet() {
return Boolean(this.projectPath);
@@ -84,7 +77,7 @@ export default {
title: this.snippet.title,
description: this.snippet.description,
visibilityLevel: this.snippet.visibilityLevel,
- files: this.getActionsEntries.filter(entry => entry.action !== ''),
+ blobActions: this.actions,
};
},
saveButtonLabel() {
@@ -95,7 +88,7 @@ export default {
},
cancelButtonHref() {
if (this.newSnippet) {
- return this.projectPath ? `/${this.projectPath}/snippets` : `/snippets`;
+ return this.projectPath ? `/${this.projectPath}/-/snippets` : `/-/snippets`;
}
return this.snippet.webUrl;
},
@@ -106,6 +99,9 @@ export default {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`;
},
},
+ beforeCreate() {
+ performance.mark(SNIPPET_MARK_EDIT_APP_START);
+ },
created() {
window.addEventListener('beforeunload', this.onBeforeUnload);
},
@@ -116,48 +112,11 @@ export default {
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
- if (!this.allBlobChangesRegistered) return undefined;
+ if (!this.hasBlobChanges || this.isUpdating) return undefined;
Object.assign(e, { returnValue });
return returnValue;
},
- updateBlobActions(args = {}) {
- // `_constants` is the internal prop that
- // should not be sent to the mutation. Hence we filter it out from
- // the argsToUpdateAction that is the data-basis for the mutation.
- const { _constants: blobConstants, ...argsToUpdateAction } = args;
- const { previousPath, filePath, content } = argsToUpdateAction;
- let actionEntry = this.blobsActions[blobConstants.id] || {};
- let tunedActions = {
- action: '',
- previousPath,
- };
-
- if (this.newSnippet) {
- // new snippet, hence new blob
- tunedActions = {
- action: SNIPPET_BLOB_ACTION_CREATE,
- previousPath: '',
- };
- } else if (previousPath && filePath) {
- // renaming of a blob + renaming & content update
- const renamedToOriginal = filePath === blobConstants.originalPath;
- tunedActions = {
- action: renamedToOriginal ? SNIPPET_BLOB_ACTION_UPDATE : SNIPPET_BLOB_ACTION_MOVE,
- previousPath: !renamedToOriginal ? blobConstants.originalPath : '',
- };
- } else if (content !== blobConstants.originalContent) {
- // content update only
- tunedActions = {
- action: SNIPPET_BLOB_ACTION_UPDATE,
- previousPath: '',
- };
- }
-
- actionEntry = { ...actionEntry, ...argsToUpdateAction, ...tunedActions };
-
- this.$set(this.blobsActions, blobConstants.id, actionEntry);
- },
flashAPIFailure(err) {
const defaultErrorMsg = this.newSnippet
? SNIPPET_CREATE_MUTATION_ERROR
@@ -214,7 +173,6 @@ export default {
if (errors.length) {
this.flashAPIFailure(errors[0]);
} else {
- this.originalContent = this.content;
redirectTo(baseObj.snippet.webUrl);
}
})
@@ -222,6 +180,9 @@ export default {
this.flashAPIFailure(e);
});
},
+ updateActions(actions) {
+ this.actions = actions;
+ },
},
newSnippetSchema: {
title: '',
@@ -257,15 +218,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
/>
- <template v-if="blobs.length">
- <snippet-blob-edit
- v-for="blob in blobs"
- :key="blob.name"
- :blob="blob"
- @blob-updated="updateBlobActions"
- />
- </template>
- <snippet-blob-edit v-else @blob-updated="updateBlobActions" />
+ <snippet-blob-actions-edit :init-blobs="blobs" @actions="updateActions" />
<snippet-visibility-edit
v-model="snippet.visibilityLevel"
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index 0779e87e6b6..ca41fd0a2b1 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -1,13 +1,16 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import SnippetHeader from './snippet_header.vue';
import SnippetTitle from './snippet_title.vue';
import SnippetBlob from './snippet_blob_view.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
+import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
import { getSnippetMixin } from '../mixins/snippets';
import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
+import { SNIPPET_MARK_VIEW_APP_START } from '~/performance_constants';
+
export default {
components: {
BlobEmbeddable,
@@ -15,12 +18,19 @@ export default {
SnippetTitle,
GlLoadingIcon,
SnippetBlob,
+ CloneDropdownButton,
},
mixins: [getSnippetMixin],
computed: {
embeddable() {
return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
},
+ canBeCloned() {
+ return Boolean(this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo);
+ },
+ },
+ beforeCreate() {
+ performance.mark(SNIPPET_MARK_VIEW_APP_START);
},
};
</script>
@@ -35,10 +45,17 @@ export default {
<template v-else>
<snippet-header :snippet="snippet" />
<snippet-title :snippet="snippet" />
- <blob-embeddable v-if="embeddable" class="gl-mb-5" :url="snippet.webUrl" />
- <div v-for="blob in blobs" :key="blob.path">
- <snippet-blob :snippet="snippet" :blob="blob" />
+ <div class="gl-display-flex gl-justify-content-end gl-mb-5">
+ <blob-embeddable v-if="embeddable" class="gl-flex-fill-1" :url="snippet.webUrl" />
+ <clone-dropdown-button
+ v-if="canBeCloned"
+ class="gl-ml-3"
+ :ssh-link="snippet.sshUrlToRepo"
+ :http-link="snippet.httpUrlToRepo"
+ data-qa-selector="clone_button"
+ />
</div>
+ <snippet-blob v-for="blob in blobs" :key="blob.path" :snippet="snippet" :blob="blob" />
</template>
</div>
</template>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
new file mode 100644
index 00000000000..55cd13a6930
--- /dev/null
+++ b/app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue
@@ -0,0 +1,156 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { cloneDeep } from 'lodash';
+import { s__, sprintf } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import SnippetBlobEdit from './snippet_blob_edit.vue';
+import { SNIPPET_MAX_BLOBS } from '../constants';
+import { createBlob, decorateBlob, diffAll } from '../utils/blob';
+
+export default {
+ components: {
+ SnippetBlobEdit,
+ GlButton,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ initBlobs: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ // This is a dictionary (by .id) of the original blobs and
+ // is used as the baseline for calculating diffs
+ // (e.g., what has been deleted, changed, renamed, etc.)
+ blobsOrig: {},
+ // This is a dictionary (by .id) of the current blobs and
+ // is updated as the user makes changes.
+ blobs: {},
+ // This is a list of blob ID's in order how they should be
+ // presented.
+ blobIds: [],
+ };
+ },
+ computed: {
+ actions() {
+ return diffAll(this.blobs, this.blobsOrig);
+ },
+ count() {
+ return this.blobIds.length;
+ },
+ addLabel() {
+ return sprintf(s__('Snippets|Add another file %{num}/%{total}'), {
+ num: this.count,
+ total: SNIPPET_MAX_BLOBS,
+ });
+ },
+ canDelete() {
+ return this.count > 1;
+ },
+ canAdd() {
+ return this.count < SNIPPET_MAX_BLOBS;
+ },
+ hasMultiFilesEnabled() {
+ return this.glFeatures.snippetMultipleFiles;
+ },
+ filesLabel() {
+ return this.hasMultiFilesEnabled ? s__('Snippets|Files') : s__('Snippets|File');
+ },
+ firstInputId() {
+ const blobId = this.blobIds[0];
+
+ if (!blobId) {
+ return '';
+ }
+
+ return `${blobId}_file_path`;
+ },
+ },
+ watch: {
+ actions: {
+ immediate: true,
+ handler(val) {
+ this.$emit('actions', val);
+ },
+ },
+ },
+ created() {
+ const blobs = this.initBlobs.map(decorateBlob);
+ const blobsById = blobs.reduce((acc, x) => Object.assign(acc, { [x.id]: x }), {});
+
+ this.blobsOrig = blobsById;
+ this.blobs = cloneDeep(blobsById);
+ this.blobIds = blobs.map(x => x.id);
+
+ // Show 1 empty blob if none exist
+ if (!this.blobIds.length) {
+ this.addBlob();
+ }
+ },
+ methods: {
+ updateBlobContent(id, content) {
+ const origBlob = this.blobsOrig[id];
+ const blob = this.blobs[id];
+
+ blob.content = content;
+
+ // If we've received content, but we haven't loaded the content before
+ // then this is also the original content.
+ if (origBlob && !origBlob.isLoaded) {
+ blob.isLoaded = true;
+ origBlob.isLoaded = true;
+ origBlob.content = content;
+ }
+ },
+ updateBlobFilePath(id, path) {
+ const blob = this.blobs[id];
+
+ blob.path = path;
+ },
+ addBlob() {
+ const blob = createBlob();
+
+ this.$set(this.blobs, blob.id, blob);
+ this.blobIds.push(blob.id);
+ },
+ deleteBlob(id) {
+ this.blobIds = this.blobIds.filter(x => x !== id);
+ this.$delete(this.blobs, id);
+ },
+ updateBlob(id, args) {
+ if ('content' in args) {
+ this.updateBlobContent(id, args.content);
+ }
+ if ('path' in args) {
+ this.updateBlobFilePath(id, args.path);
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div class="form-group file-editor">
+ <label :for="firstInputId">{{ filesLabel }}</label>
+ <snippet-blob-edit
+ v-for="(blobId, index) in blobIds"
+ :key="blobId"
+ :class="{ 'gl-mt-3': index > 0 }"
+ :blob="blobs[blobId]"
+ :can-delete="canDelete"
+ :show-delete="hasMultiFilesEnabled"
+ @blob-updated="updateBlob(blobId, $event)"
+ @delete="deleteBlob(blobId)"
+ />
+ <gl-button
+ v-if="hasMultiFilesEnabled"
+ :disabled="!canAdd"
+ data-testid="add_button"
+ class="gl-my-3"
+ variant="dashed"
+ @click="addBlob"
+ >{{ addLabel }}</gl-button
+ >
+ </div>
+</template>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index 3c2dbfff6e1..ff03432f942 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -1,19 +1,13 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import { SNIPPET_BLOB_CONTENT_FETCH_ERROR } from '~/snippets/constants';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { sprintf } from '~/locale';
-function localId() {
- return Math.floor((1 + Math.random()) * 0x10000)
- .toString(16)
- .substring(1);
-}
-
export default {
components: {
BlobHeaderEdit,
@@ -24,49 +18,35 @@ export default {
props: {
blob: {
type: Object,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
required: false,
- default: null,
- validator: ({ rawPath }) => Boolean(rawPath),
+ default: true,
},
- },
- data() {
- return {
- id: localId(),
- filePath: this.blob?.path || '',
- previousPath: '',
- originalPath: this.blob?.path || '',
- content: this.blob?.content || '',
- originalContent: '',
- isContentLoading: this.blob,
- };
- },
- watch: {
- filePath(filePath, previousPath) {
- this.previousPath = previousPath;
- this.notifyAboutUpdates({ previousPath });
+ showDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- content() {
- this.notifyAboutUpdates();
+ },
+ computed: {
+ inputId() {
+ return `${this.blob.id}_file_path`;
},
},
mounted() {
- if (this.blob) {
+ if (!this.blob.isLoaded) {
this.fetchBlobContent();
}
},
methods: {
+ onDelete() {
+ this.$emit('delete');
+ },
notifyAboutUpdates(args = {}) {
- const { filePath, previousPath } = args;
- this.$emit('blob-updated', {
- filePath: filePath || this.filePath,
- previousPath: previousPath || this.previousPath,
- content: this.content,
- _constants: {
- originalPath: this.originalPath,
- originalContent: this.originalContent,
- id: this.id,
- },
- });
+ this.$emit('blob-updated', args);
},
fetchBlobContent() {
const baseUrl = getBaseURL();
@@ -75,33 +55,39 @@ export default {
axios
.get(url)
.then(res => {
- this.originalContent = res.data;
- this.content = res.data;
+ this.notifyAboutUpdates({ content: res.data });
})
- .catch(e => this.flashAPIFailure(e))
- .finally(() => {
- this.isContentLoading = false;
- });
+ .catch(e => this.flashAPIFailure(e));
},
flashAPIFailure(err) {
Flash(sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }));
- this.isContentLoading = false;
},
},
};
</script>
<template>
- <div class="form-group file-editor">
- <label>{{ s__('Snippets|File') }}</label>
- <div class="file-holder snippet">
- <blob-header-edit v-model="filePath" data-qa-selector="file_name_field" />
- <gl-loading-icon
- v-if="isContentLoading"
- :label="__('Loading snippet')"
- size="lg"
- class="loading-animation prepend-top-20 append-bottom-20"
- />
- <blob-content-edit v-else v-model="content" :file-name="filePath" />
- </div>
+ <div class="file-holder snippet">
+ <blob-header-edit
+ :id="inputId"
+ :value="blob.path"
+ data-qa-selector="file_name_field"
+ :can-delete="canDelete"
+ :show-delete="showDelete"
+ @input="notifyAboutUpdates({ path: $event })"
+ @delete="onDelete"
+ />
+ <gl-loading-icon
+ v-if="!blob.isLoaded"
+ :label="__('Loading snippet')"
+ size="lg"
+ class="loading-animation prepend-top-20 append-bottom-20"
+ />
+ <blob-content-edit
+ v-else
+ :value="blob.content"
+ :file-global-id="blob.id"
+ :file-name="blob.path"
+ @input="notifyAboutUpdates({ content: $event })"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index afd038eef58..b38be5bb9a4 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -1,7 +1,6 @@
<script>
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContent from '~/blob/components/blob_content.vue';
-import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
import GetBlobContent from '../queries/snippet.blob.content.query.graphql';
@@ -16,7 +15,6 @@ export default {
components: {
BlobHeader,
BlobContent,
- CloneDropdownButton,
},
apollo: {
blobContent: {
@@ -27,8 +25,9 @@ export default {
rich: this.activeViewerType === RICH_BLOB_VIEWER,
};
},
- update: data =>
- data.snippets.edges[0].node.blob.richData || data.snippets.edges[0].node.blob.plainData,
+ update(data) {
+ return this.onContentUpdate(data);
+ },
result() {
if (this.activeViewerType === RICH_BLOB_VIEWER) {
this.blob.richViewer.renderError = null;
@@ -66,9 +65,6 @@ export default {
const { richViewer, simpleViewer } = this.blob;
return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
},
- canBeCloned() {
- return this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo;
- },
hasRenderError() {
return Boolean(this.viewer.renderError);
},
@@ -81,6 +77,12 @@ export default {
this.$apollo.queries.blobContent.skip = false;
this.$apollo.queries.blobContent.refetch();
},
+ onContentUpdate(data) {
+ const { path: blobPath } = this.blob;
+ const { blobs } = data.snippets.edges[0].node;
+ const updatedBlobData = blobs.find(blob => blob.path === blobPath);
+ return updatedBlobData.richData || updatedBlobData.plainData;
+ },
},
BLOB_RENDER_EVENT_LOAD,
BLOB_RENDER_EVENT_SHOW_SOURCE,
@@ -93,17 +95,7 @@ export default {
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@viewer-changed="switchViewer"
- >
- <template #actions>
- <clone-dropdown-button
- v-if="canBeCloned"
- class="gl-mr-3"
- :ssh-link="snippet.sshUrlToRepo"
- :http-link="snippet.httpUrlToRepo"
- data-qa-selector="clone_button"
- />
- </template>
- </blob-header>
+ />
<blob-content
:loading="isContentLoading"
:content="blobContent"
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 707e2b0ea30..ed087dcfaf9 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -1,5 +1,4 @@
<script>
-import { __ } from '~/locale';
import {
GlAvatar,
GlIcon,
@@ -7,11 +6,12 @@ import {
GlModal,
GlAlert,
GlLoadingIcon,
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
+import { __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
@@ -26,8 +26,8 @@ export default {
GlModal,
GlAlert,
GlLoadingIcon,
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
TimeAgoTooltip,
GlButton,
},
@@ -68,6 +68,11 @@ export default {
snippetHasBinary() {
return Boolean(this.snippet.blobs.find(blob => blob.binary));
},
+ authoredMessage() {
+ return this.snippet.author
+ ? __('Authored %{timeago} by %{author}')
+ : __('Authored %{timeago}');
+ },
personalSnippetActions() {
return [
{
@@ -91,8 +96,8 @@ export default {
condition: this.canCreateSnippet,
text: __('New snippet'),
href: this.snippet.project
- ? `${this.snippet.project.webUrl}/snippets/new`
- : '/snippets/new',
+ ? `${this.snippet.project.webUrl}/-/snippets/new`
+ : '/-/snippets/new',
variant: 'success',
category: 'secondary',
cssClass: 'ml-2',
@@ -130,7 +135,9 @@ export default {
},
methods: {
redirectToSnippets() {
- window.location.pathname = `${this.snippet.project?.fullPath || 'dashboard'}/snippets`;
+ window.location.pathname = this.snippet.project
+ ? `${this.snippet.project.fullPath}/-/snippets`
+ : 'dashboard/snippets';
},
closeDeleteModal() {
this.$refs.deleteModal.hide();
@@ -176,8 +183,8 @@ export default {
</span>
<gl-icon :name="visibilityLevelIcon" :size="14" />
</div>
- <div class="creator">
- <gl-sprintf :message="__('Authored %{timeago} by %{author}')">
+ <div class="creator" data-testid="authored-message">
+ <gl-sprintf :message="authoredMessage">
<template #timeago>
<time-ago-tooltip
:time="snippet.createdAt"
@@ -221,17 +228,17 @@ export default {
</template>
</div>
<div class="d-block d-sm-none dropdown">
- <gl-dropdown :text="__('Options')" class="w-100" toggle-class="text-center">
- <gl-dropdown-item
+ <gl-deprecated-dropdown :text="__('Options')" class="w-100" toggle-class="text-center">
+ <gl-deprecated-dropdown-item
v-for="(action, index) in personalSnippetActions"
:key="index"
:disabled="action.disabled"
:title="action.title"
:href="action.href"
@click="action.click ? action.click() : undefined"
- >{{ action.text }}</gl-dropdown-item
+ >{{ action.text }}</gl-deprecated-dropdown-item
>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</div>
</div>
diff --git a/app/assets/javascripts/snippets/constants.js b/app/assets/javascripts/snippets/constants.js
index 99ee698408d..12b83525bf7 100644
--- a/app/assets/javascripts/snippets/constants.js
+++ b/app/assets/javascripts/snippets/constants.js
@@ -30,3 +30,6 @@ export const SNIPPET_BLOB_CONTENT_FETCH_ERROR = __("Can't fetch content for the
export const SNIPPET_BLOB_ACTION_CREATE = 'create';
export const SNIPPET_BLOB_ACTION_UPDATE = 'update';
export const SNIPPET_BLOB_ACTION_MOVE = 'move';
+export const SNIPPET_BLOB_ACTION_DELETE = 'delete';
+
+export const SNIPPET_MAX_BLOBS = 10;
diff --git a/app/assets/javascripts/snippets/index.js b/app/assets/javascripts/snippets/index.js
index 1c79492957d..bb5e7d6e3f0 100644
--- a/app/assets/javascripts/snippets/index.js
+++ b/app/assets/javascripts/snippets/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
import VueApollo from 'vue-apollo';
+import Translate from '~/vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
import SnippetsShow from './components/show.vue';
@@ -38,5 +38,3 @@ export const SnippetShowInit = () => {
export const SnippetEditInit = () => {
appFactory(document.getElementById('js-snippet-edit'), SnippetsEdit);
};
-
-export default () => {};
diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js
index 91331cdf339..3f5d64a768f 100644
--- a/app/assets/javascripts/snippets/mixins/snippets.js
+++ b/app/assets/javascripts/snippets/mixins/snippets.js
@@ -2,6 +2,7 @@ import GetSnippetQuery from '../queries/snippet.query.graphql';
const blobsDefault = [];
+// eslint-disable-next-line import/prefer-default-export
export const getSnippetMixin = {
apollo: {
snippet: {
@@ -39,5 +40,3 @@ export const getSnippetMixin = {
},
},
};
-
-export default () => {};
diff --git a/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql b/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql
index 889a88dd93c..8f1f16b76c2 100644
--- a/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql
+++ b/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql
@@ -3,7 +3,8 @@ query SnippetBlobContent($ids: [ID!], $rich: Boolean!) {
edges {
node {
id
- blob {
+ blobs {
+ path
richData @include(if: $rich)
plainData @skip(if: $rich)
}
diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js
new file mode 100644
index 00000000000..fd5ff9a3d2e
--- /dev/null
+++ b/app/assets/javascripts/snippets/utils/blob.js
@@ -0,0 +1,66 @@
+import { uniqueId } from 'lodash';
+import {
+ SNIPPET_BLOB_ACTION_CREATE,
+ SNIPPET_BLOB_ACTION_UPDATE,
+ SNIPPET_BLOB_ACTION_MOVE,
+ SNIPPET_BLOB_ACTION_DELETE,
+} from '../constants';
+
+const createLocalId = () => uniqueId('blob_local_');
+
+export const decorateBlob = blob => ({
+ ...blob,
+ id: createLocalId(),
+ isLoaded: false,
+ content: '',
+});
+
+export const createBlob = () => ({
+ id: createLocalId(),
+ content: '',
+ path: '',
+ isLoaded: true,
+});
+
+const diff = ({ content, path }, origBlob) => {
+ if (!origBlob) {
+ return {
+ action: SNIPPET_BLOB_ACTION_CREATE,
+ previousPath: path,
+ content,
+ filePath: path,
+ };
+ } else if (origBlob.path !== path || origBlob.content !== content) {
+ return {
+ action: origBlob.path === path ? SNIPPET_BLOB_ACTION_UPDATE : SNIPPET_BLOB_ACTION_MOVE,
+ previousPath: origBlob.path,
+ content,
+ filePath: path,
+ };
+ }
+
+ return null;
+};
+
+/**
+ * This function returns an array of diff actions (to be sent to the BE) based on the current vs. original blobs
+ *
+ * @param {Object} blobs
+ * @param {Object} origBlobs
+ */
+export const diffAll = (blobs, origBlobs) => {
+ const deletedEntries = Object.values(origBlobs)
+ .filter(x => !blobs[x.id])
+ .map(({ path, content }) => ({
+ action: SNIPPET_BLOB_ACTION_DELETE,
+ previousPath: path,
+ filePath: path,
+ content,
+ }));
+
+ const newEntries = Object.values(blobs)
+ .map(blob => diff(blob, origBlobs[blob.id]))
+ .filter(x => x);
+
+ return [...deletedEntries, ...newEntries];
+};
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 97afeecd8ac..64842ae7f8d 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import Flash from './flash';
+import { deprecatedCreateFlash as Flash } from './flash';
import { __, s__ } from './locale';
import { spriteIcon } from './lib/utils/common_utils';
import axios from './lib/utils/axios_utils';
diff --git a/app/assets/javascripts/static_site_editor/components/app.vue b/app/assets/javascripts/static_site_editor/components/app.vue
index 98240aef810..365fc7ce6e9 100644
--- a/app/assets/javascripts/static_site_editor/components/app.vue
+++ b/app/assets/javascripts/static_site_editor/components/app.vue
@@ -1,3 +1,13 @@
+<script>
+export default {
+ props: {
+ mergeRequestsIllustrationPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
<template>
- <router-view />
+ <router-view :merge-requests-illustration-path="mergeRequestsIllustrationPath" />
</template>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index 84a16f327d9..53fbb2a330d 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -7,6 +7,8 @@ import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants';
import { DEFAULT_IMAGE_UPLOAD_PATH } from '../constants';
import imageRepository from '../image_repository';
+import formatter from '../services/formatter';
+import templater from '../services/templater';
export default {
components: {
@@ -43,7 +45,7 @@ export default {
data() {
return {
saveable: false,
- parsedSource: parseSourceFile(this.content),
+ parsedSource: parseSourceFile(this.preProcess(true, this.content)),
editorMode: EDITOR_TYPES.wysiwyg,
isModified: false,
};
@@ -58,20 +60,30 @@ export default {
},
},
methods: {
+ preProcess(isWrap, value) {
+ const formattedContent = formatter(value);
+ const templatedContent = isWrap
+ ? templater.wrap(formattedContent)
+ : templater.unwrap(formattedContent);
+ return templatedContent;
+ },
onInputChange(newVal) {
this.parsedSource.sync(newVal, this.isWysiwygMode);
this.isModified = this.parsedSource.isModified();
},
onModeChange(mode) {
this.editorMode = mode;
- this.$refs.editor.resetInitialValue(this.editableContent);
+
+ const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent);
+ this.$refs.editor.resetInitialValue(preProcessedContent);
},
onUploadImage({ file, imageUrl }) {
this.$options.imageRepository.add(file, imageUrl);
},
onSubmit() {
+ const preProcessedContent = this.preProcess(false, this.parsedSource.content());
this.$emit('submit', {
- content: this.parsedSource.content(),
+ content: preProcessedContent,
images: this.$options.imageRepository.getAll(),
});
},
diff --git a/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue b/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue
deleted file mode 100644
index dd907570114..00000000000
--- a/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<script>
-import { isString } from 'lodash';
-
-import { GlLink, GlButton } from '@gitlab/ui';
-
-const validateUrlAndLabel = value => isString(value.label) && isString(value.url);
-
-export default {
- components: {
- GlLink,
- GlButton,
- },
- props: {
- branch: {
- type: Object,
- required: true,
- validator: validateUrlAndLabel,
- },
- commit: {
- type: Object,
- required: true,
- validator: validateUrlAndLabel,
- },
- mergeRequest: {
- type: Object,
- required: true,
- validator: validateUrlAndLabel,
- },
- returnUrl: {
- type: String,
- required: false,
- default: '',
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div class="border-bottom pb-4">
- <h3>{{ s__('StaticSiteEditor|Success!') }}</h3>
- <p>
- {{
- s__(
- 'StaticSiteEditor|Your changes have been submitted and a merge request has been created. The changes won’t be visible on the site until the merge request has been accepted.',
- )
- }}
- </p>
- <div class="d-flex justify-content-end">
- <gl-button v-if="returnUrl" ref="returnToSiteButton" :href="returnUrl">{{
- s__('StaticSiteEditor|Return to site')
- }}</gl-button>
- <gl-button ref="mergeRequestButton" class="ml-2" :href="mergeRequest.url" variant="success">
- {{ s__('StaticSiteEditor|View merge request') }}
- </gl-button>
- </div>
- </div>
-
- <div class="pt-2">
- <h4>{{ s__('StaticSiteEditor|Summary of changes') }}</h4>
- <ul>
- <li>
- {{ s__('StaticSiteEditor|You created a new branch:') }}
- <gl-link ref="branchLink" :href="branch.url">{{ branch.label }}</gl-link>
- </li>
- <li>
- {{ s__('StaticSiteEditor|You created a merge request:') }}
- <gl-link ref="mergeRequestLink" :href="mergeRequest.url">{{
- mergeRequest.label
- }}</gl-link>
- </li>
- <li>
- {{ s__('StaticSiteEditor|You added a commit:') }}
- <gl-link ref="commitLink" :href="commit.url">{{ commit.label }}</gl-link>
- </li>
- </ul>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/image_repository.js b/app/assets/javascripts/static_site_editor/image_repository.js
index 541d581bda8..02285ccdba3 100644
--- a/app/assets/javascripts/static_site_editor/image_repository.js
+++ b/app/assets/javascripts/static_site_editor/image_repository.js
@@ -1,5 +1,5 @@
import { __ } from '~/locale';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import { getBinary } from './services/image_service';
const imageRepository = () => {
diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js
index 12aa301e02f..b7e5ea4eee3 100644
--- a/app/assets/javascripts/static_site_editor/index.js
+++ b/app/assets/javascripts/static_site_editor/index.js
@@ -5,7 +5,14 @@ import createRouter from './router';
import createApolloProvider from './graphql';
const initStaticSiteEditor = el => {
- const { isSupportedContent, path: sourcePath, baseUrl, namespace, project } = el.dataset;
+ const {
+ isSupportedContent,
+ path: sourcePath,
+ baseUrl,
+ namespace,
+ project,
+ mergeRequestsIllustrationPath,
+ } = el.dataset;
const { current_username: username } = window.gon;
const returnUrl = el.dataset.returnUrl || null;
@@ -26,7 +33,11 @@ const initStaticSiteEditor = el => {
App,
},
render(createElement) {
- return createElement('app');
+ return createElement('app', {
+ props: {
+ mergeRequestsIllustrationPath,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue
index 156b815e07a..eef2bd88f0e 100644
--- a/app/assets/javascripts/static_site_editor/pages/home.vue
+++ b/app/assets/javascripts/static_site_editor/pages/home.vue
@@ -6,7 +6,7 @@ import SubmitChangesError from '../components/submit_changes_error.vue';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
import sourceContentQuery from '../graphql/queries/source_content.query.graphql';
import submitContentChangesMutation from '../graphql/mutations/submit_content_changes.mutation.graphql';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import Tracking from '~/tracking';
import { LOAD_CONTENT_ERROR, TRACKING_ACTION_INITIALIZE_EDITOR } from '../constants';
import { SUCCESS_ROUTE } from '../router/constants';
diff --git a/app/assets/javascripts/static_site_editor/pages/success.vue b/app/assets/javascripts/static_site_editor/pages/success.vue
index 123683b2833..f0d597d7c9b 100644
--- a/app/assets/javascripts/static_site_editor/pages/success.vue
+++ b/app/assets/javascripts/static_site_editor/pages/success.vue
@@ -1,12 +1,21 @@
<script>
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { s__, __, sprintf } from '~/locale';
+
import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
import appDataQuery from '../graphql/queries/app_data.query.graphql';
-import SavedChangesMessage from '../components/saved_changes_message.vue';
import { HOME_ROUTE } from '../router/constants';
export default {
components: {
- SavedChangesMessage,
+ GlEmptyState,
+ GlButton,
+ },
+ props: {
+ mergeRequestsIllustrationPath: {
+ type: String,
+ required: true,
+ },
},
apollo: {
savedContentMeta: {
@@ -16,20 +25,65 @@ export default {
query: appDataQuery,
},
},
+ computed: {
+ updatedFileDescription() {
+ const { sourcePath } = this.appData;
+
+ return sprintf(s__('Update %{sourcePath} file'), { sourcePath });
+ },
+ },
created() {
if (!this.savedContentMeta) {
this.$router.push(HOME_ROUTE);
}
},
+ title: s__('StaticSiteEditor|Your merge request has been created'),
+ primaryButtonText: __('View merge request'),
+ returnToSiteBtnText: s__('StaticSiteEditor|Return to site'),
+ mergeRequestInstructionsHeading: s__(
+ 'StaticSiteEditor|To see your changes live you will need to do the following things:',
+ ),
+ addTitleInstruction: s__('StaticSiteEditor|1. Add a clear title to describe the change.'),
+ addDescriptionInstruction: s__(
+ 'StaticSiteEditor|2. Add a description to explain why the change is being made.',
+ ),
+ assignMergeRequestInstruction: s__(
+ 'StaticSiteEditor|3. Assign a person to review and accept the merge request.',
+ ),
};
</script>
<template>
- <div v-if="savedContentMeta" class="container">
- <saved-changes-message
- :branch="savedContentMeta.branch"
- :commit="savedContentMeta.commit"
- :merge-request="savedContentMeta.mergeRequest"
- :return-url="appData.returnUrl"
- />
+ <div
+ v-if="savedContentMeta"
+ class="container gl-flex-grow-1 gl-display-flex gl-flex-direction-column"
+ >
+ <div class="gl-fixed gl-left-0 gl-right-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100">
+ <div class="container gl-py-4">
+ <gl-button
+ v-if="appData.returnUrl"
+ ref="returnToSiteButton"
+ class="gl-mr-5"
+ :href="appData.returnUrl"
+ >{{ $options.returnToSiteBtnText }}</gl-button
+ >
+ <strong>
+ {{ updatedFileDescription }}
+ </strong>
+ </div>
+ </div>
+ <gl-empty-state
+ class="gl-my-9"
+ :primary-button-text="$options.primaryButtonText"
+ :title="$options.title"
+ :primary-button-link="savedContentMeta.mergeRequest.url"
+ :svg-path="mergeRequestsIllustrationPath"
+ >
+ <template #description>
+ <p>{{ $options.mergeRequestInstructionsHeading }}</p>
+ <p>{{ $options.addTitleInstruction }}</p>
+ <p>{{ $options.addDescriptionInstruction }}</p>
+ <p>{{ $options.assignMergeRequestInstruction }}</p>
+ </template>
+ </gl-empty-state>
</div>
</template>
diff --git a/app/assets/javascripts/static_site_editor/services/formatter.js b/app/assets/javascripts/static_site_editor/services/formatter.js
new file mode 100644
index 00000000000..92d5e8a5df8
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/services/formatter.js
@@ -0,0 +1,14 @@
+const removeOrphanedBrTags = source => {
+ /* Until the underlying Squire editor of Toast UI Editor resolves duplicate `<br>` tags, this
+ `replace` solution will clear out orphaned `<br>` tags that it generates. Additionally,
+ it cleans up orphaned `<br>` tags in the source markdown document that should be new lines.
+ https://gitlab.com/gitlab-org/gitlab/-/issues/227602#note_380765330
+ */
+ return source.replace(/\n^<br>$/gm, '');
+};
+
+const format = source => {
+ return removeOrphanedBrTags(source);
+};
+
+export default format;
diff --git a/app/assets/javascripts/static_site_editor/services/templater.js b/app/assets/javascripts/static_site_editor/services/templater.js
new file mode 100644
index 00000000000..a1c1bb6b8d6
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/services/templater.js
@@ -0,0 +1,89 @@
+/**
+ * The purpose of this file is to modify Markdown source such that templated code (embedded ruby currently) can be temporarily wrapped and unwrapped in codeblocks:
+ * 1. `wrap()`: temporarily wrap in codeblocks (useful for a WYSIWYG editing experience)
+ * 2. `unwrap()`: undo the temporarily wrapped codeblocks (useful for Markdown editing experience and saving edits)
+ *
+ * Without this `templater`, the templated code is otherwise interpreted as Markdown content resulting in loss of spacing, indentation, escape characters, etc.
+ *
+ */
+
+const ticks = '```';
+const marker = 'sse';
+const wrapPrefix = `${ticks} ${marker}\n`; // Space intentional due to https://github.com/nhn/tui.editor/blob/6bcec75c69028570d93d973aa7533090257eaae0/libs/to-mark/src/renderer.gfm.js#L26
+const wrapPostfix = `\n${ticks}`;
+const markPrefix = `${marker}-${Date.now()}`;
+
+const reHelpers = {
+ template: `.| |\\t|\\n(?!(\\n|${markPrefix}))`,
+ openTag: '<[a-zA-Z]+.*?>',
+ closeTag: '</.+>',
+};
+const reTemplated = new RegExp(`(^${wrapPrefix}(${reHelpers.template})+?${wrapPostfix}$)`, 'gm');
+const rePreexistingCodeBlocks = new RegExp(`(^${ticks}.*\\n(.|\\s)+?${ticks}$)`, 'gm');
+const reHtmlMarkup = new RegExp(
+ `^((${reHelpers.openTag}){1}(${reHelpers.template})*(${reHelpers.closeTag}){1})$`,
+ 'gm',
+);
+const reEmbeddedRubyBlock = new RegExp(`(^<%(${reHelpers.template})+%>$)`, 'gm');
+const reEmbeddedRubyInline = new RegExp(`(^.*[<|&lt;]%(${reHelpers.template})+$)`, 'gm');
+
+const patternGroups = {
+ ignore: [rePreexistingCodeBlocks],
+ // Order is intentional (general to specific) where HTML markup is marked first, then ERB blocks, then inline ERB
+ // Order in combo with the `mark()` algorithm is used to mitigate potential duplicate pattern matches (ERB nested in HTML for example)
+ allow: [reHtmlMarkup, reEmbeddedRubyBlock, reEmbeddedRubyInline],
+};
+
+const mark = (source, groups) => {
+ let text = source;
+ let id = 0;
+ const hash = {};
+
+ Object.entries(groups).forEach(([groupKey, group]) => {
+ group.forEach(pattern => {
+ const matches = text.match(pattern);
+ if (matches) {
+ matches.forEach(match => {
+ const key = `${markPrefix}-${groupKey}-${id}`;
+ text = text.replace(match, key);
+ hash[key] = match;
+ id += 1;
+ });
+ }
+ });
+ });
+
+ return { text, hash };
+};
+
+const unmark = (text, hash) => {
+ let source = text;
+
+ Object.entries(hash).forEach(([key, value]) => {
+ const newVal = key.includes('ignore') ? value : `${wrapPrefix}${value}${wrapPostfix}`;
+ source = source.replace(key, newVal);
+ });
+
+ return source;
+};
+
+const unwrap = source => {
+ let text = source;
+ const matches = text.match(reTemplated);
+
+ if (matches) {
+ matches.forEach(match => {
+ const initial = match.replace(`${wrapPrefix}`, '').replace(`${wrapPostfix}`, '');
+ text = text.replace(match, initial);
+ });
+ }
+
+ return text;
+};
+
+const wrap = source => {
+ const { text, hash } = mark(unwrap(source), patternGroups);
+ return unmark(text, hash);
+};
+
+export default { wrap, unwrap };
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index 5172a1ef3d6..f2b05946a08 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import 'deckar01-task_list';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
-import Flash from './flash';
+import { deprecatedCreateFlash as Flash } from './flash';
export default class TaskList {
constructor(options = {}) {
diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js
index bcb44bf7acf..10a9d29e694 100644
--- a/app/assets/javascripts/toggle_buttons.js
+++ b/app/assets/javascripts/toggle_buttons.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import Flash from './flash';
+import { deprecatedCreateFlash as Flash } from './flash';
import { __ } from './locale';
import { parseBoolean } from './lib/utils/common_utils';
diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/usage_ping_consent.js
index 1e7a5fb19c2..a69620c1762 100644
--- a/app/assets/javascripts/usage_ping_consent.js
+++ b/app/assets/javascripts/usage_ping_consent.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
-import Flash, { hideFlash } from './flash';
+import { deprecatedCreateFlash as Flash, hideFlash } from './flash';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 290de55e6f9..c8f95dac48e 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index f72de8c2f4d..e45b0de9083 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -4,14 +4,14 @@
import $ from 'jquery';
import { escape, template, uniqBy } from 'lodash';
-import axios from '../lib/utils/axios_utils';
-import { s__, __, sprintf } from '../locale';
-import ModalStore from '../boards/stores/modal_store';
-import { parseBoolean } from '../lib/utils/common_utils';
import {
AJAX_USERS_SELECT_OPTIONS_MAP,
AJAX_USERS_SELECT_PARAMS_MAP,
} from 'ee_else_ce/users_select/constants';
+import axios from '../lib/utils/axios_utils';
+import { s__, __, sprintf } from '../locale';
+import ModalStore from '../boards/stores/modal_store';
+import { parseBoolean } from '../lib/utils/common_utils';
import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
// TODO: remove eventHub hack after code splitting refactor
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 0f9d1b8395b..7297f8f8677 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 } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__ } from '~/locale';
import eventHub from '../../event_hub';
import approvalsMixin from '../../mixins/approvals';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
index 66af0c5a83e..24cd9d6428d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue
@@ -1,10 +1,6 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
-import {
- OPTIONAL,
- OPTIONAL_CAN_APPROVE,
-} from '~/vue_merge_request_widget/components/approvals/messages';
export default {
components: {
@@ -25,17 +21,12 @@ export default {
default: '',
},
},
- computed: {
- message() {
- return this.canApprove ? OPTIONAL_CAN_APPROVE : OPTIONAL;
- },
- },
};
</script>
<template>
<div class="d-flex align-items-center">
- <span class="text-muted">{{ message }}</span>
+ <span class="text-muted">{{ s__('mrWidget|Approval is optional') }}</span>
<gl-link
v-if="canApprove && helpPath"
v-gl-tooltip
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js b/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
index 1d9368f71aa..0538c38307b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/messages.js
@@ -7,5 +7,3 @@ export const FETCH_ERROR = s__(
export const APPROVE_ERROR = s__('mrWidget|An error occurred while submitting your approval.');
export const UNAPPROVE_ERROR = s__('mrWidget|An error occurred while removing your approval.');
export const APPROVED_MESSAGE = s__('mrWidget|Merge request approved.');
-export const OPTIONAL_CAN_APPROVE = s__('mrWidget|No approval required; you can still approve');
-export const OPTIONAL = s__('mrWidget|No approval required');
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 573fc388cca..af0b4087d46 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,7 +1,7 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MRWidgetService from '../../services/mr_widget_service';
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 bce25ca20ec..b12250d1d1c 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
@@ -60,28 +60,24 @@ export default {
:main-action-link="deploymentExternalUrl"
filter-key="path"
>
- <template slot="mainAction" slot-scope="slotProps">
+ <template #mainAction="{ className }">
<review-app-link
:display="appButtonText"
:link="deploymentExternalUrl"
- :css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
+ :css-class="`deploy-link js-deploy-url inline ${className}`"
/>
</template>
- <template slot="result" slot-scope="slotProps">
+ <template #result="{ result }">
<gl-link
- :href="slotProps.result.external_url"
+ :href="result.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
class="js-deploy-url-menu-item menu-item"
>
- <strong class="str-truncated-100 gl-mb-0 d-block">
- {{ slotProps.result.path }}
- </strong>
+ <strong class="str-truncated-100 gl-mb-0 d-block">{{ result.path }}</strong>
- <p class="text-secondary str-truncated-100 gl-mb-0 d-block">
- {{ slotProps.result.external_url }}
- </p>
+ <p class="text-secondary str-truncated-100 gl-mb-0 d-block">{{ result.external_url }}</p>
</gl-link>
</template>
</filtered-search-dropdown>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue
index fd999540f4a..c368399ed6f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_expandable_section.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
/**
* Renders header section with icon and expand button
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index a096eb1a1fe..7326bd0804d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -84,9 +84,6 @@ export default {
hasCommitInfo() {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
},
- isTriggeredByMergeRequest() {
- return Boolean(this.pipeline.merge_request);
- },
isMergeRequestPipeline() {
return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline);
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index de01821a292..936fdc9aff5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -2,28 +2,36 @@
import { GlLink, GlSprintf, GlButton } from '@gitlab/ui';
import MrWidgetIcon from './mr_widget_icon.vue';
import Tracking from '~/tracking';
-import { s__ } from '~/locale';
+import DismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
+import {
+ SP_TRACK_LABEL,
+ SP_LINK_TRACK_EVENT,
+ SP_SHOW_TRACK_EVENT,
+ SP_LINK_TRACK_VALUE,
+ SP_SHOW_TRACK_VALUE,
+ SP_HELP_CONTENT,
+ SP_HELP_URL,
+ SP_ICON_NAME,
+} from '../constants';
const trackingMixin = Tracking.mixin();
-const TRACK_LABEL = 'no_pipeline_noticed';
export default {
name: 'MRWidgetSuggestPipeline',
- iconName: 'status_notfound',
- trackLabel: TRACK_LABEL,
- linkTrackValue: 30,
- linkTrackEvent: 'click_link',
- showTrackValue: 10,
- showTrackEvent: 'click_button',
- helpContent: s__(
- `mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd} by simply adding a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust.`,
- ),
- helpURL: 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/',
+ SP_ICON_NAME,
+ SP_TRACK_LABEL,
+ SP_LINK_TRACK_EVENT,
+ SP_SHOW_TRACK_EVENT,
+ SP_LINK_TRACK_VALUE,
+ SP_SHOW_TRACK_VALUE,
+ SP_HELP_CONTENT,
+ SP_HELP_URL,
components: {
GlLink,
GlSprintf,
GlButton,
MrWidgetIcon,
+ DismissibleContainer,
},
mixins: [trackingMixin],
props: {
@@ -39,11 +47,19 @@ export default {
type: String,
required: true,
},
+ userCalloutsPath: {
+ type: String,
+ required: true,
+ },
+ userCalloutFeatureId: {
+ type: String,
+ required: true,
+ },
},
computed: {
tracking() {
return {
- label: TRACK_LABEL,
+ label: SP_TRACK_LABEL,
property: this.humanAccess,
};
},
@@ -54,9 +70,14 @@ export default {
};
</script>
<template>
- <div class="mr-widget-body mr-pipeline-suggest gl-mb-3">
- <div class="gl-display-flex gl-align-items-center">
- <mr-widget-icon :name="$options.iconName" />
+ <dismissible-container
+ class="mr-widget-body mr-pipeline-suggest gl-mb-3"
+ :path="userCalloutsPath"
+ :feature-id="userCalloutFeatureId"
+ @dismiss="$emit('dismiss')"
+ >
+ <template #title>
+ <mr-widget-icon :name="$options.SP_ICON_NAME" />
<div>
<gl-sprintf
:message="
@@ -76,18 +97,18 @@ export default {
class="gl-ml-1"
data-testid="add-pipeline-link"
:data-track-property="humanAccess"
- :data-track-value="$options.linkTrackValue"
- :data-track-event="$options.linkTrackEvent"
- :data-track-label="$options.trackLabel"
+ :data-track-value="$options.SP_LINK_TRACK_VALUE"
+ :data-track-event="$options.SP_LINK_TRACK_EVENT"
+ :data-track-label="$options.SP_TRACK_LABEL"
>
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</div>
- </div>
+ </template>
<div class="row">
- <div class="col-md-5 order-md-last col-12 gl-mt-5 mt-md-n3 svg-content svg-225">
+ <div class="col-md-5 order-md-last col-12 gl-mt-5 mt-md-n1 pt-md-1 svg-content svg-225">
<img data-testid="pipeline-image" :src="pipelineSvgPath" />
</div>
<div class="col-md-7 order-md-first col-12">
@@ -96,11 +117,11 @@ export default {
{{ s__('mrWidget|Are you adding technical debt or code vulnerabilities?') }}
</strong>
<p class="gl-mt-2">
- <gl-sprintf :message="$options.helpContent">
+ <gl-sprintf :message="$options.SP_HELP_CONTENT">
<template #link="{ content }">
<gl-link
data-testid="help"
- :href="$options.helpURL"
+ :href="$options.SP_HELP_URL"
target="_blank"
class="font-size-inherit"
>{{ content }}
@@ -115,14 +136,14 @@ export default {
variant="info"
:href="pipelinePath"
:data-track-property="humanAccess"
- :data-track-value="$options.showTrackValue"
- :data-track-event="$options.showTrackEvent"
- :data-track-label="$options.trackLabel"
+ :data-track-value="$options.SP_SHOW_TRACK_VALUE"
+ :data-track-event="$options.SP_SHOW_TRACK_EVENT"
+ :data-track-label="$options.SP_TRACK_LABEL"
>
{{ __('Show me how to add a pipeline') }}
</gl-button>
</div>
</div>
</div>
- </div>
+ </dismissible-container>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
index a0e76b151f7..dab0540f44e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue
@@ -1,22 +1,37 @@
<script>
+import { GlSprintf } from '@gitlab/ui';
import tooltip from '../../vue_shared/directives/tooltip';
import { __ } from '../../locale';
export default {
+ i18n: {
+ removesBranchText: __('%{strongStart}Deletes%{strongEnd} source branch'),
+ tooltipTitle: __('A user with write access to the source branch selected this option'),
+ },
+ components: {
+ GlSprintf,
+ },
directives: {
tooltip,
},
- created() {
- this.removesBranchText = __('<strong>Deletes</strong> source branch');
- this.tooltipTitle = __('A user with write access to the source branch selected this option');
- },
};
</script>
<template>
<p v-once class="mr-info-list mr-links gl-mb-0">
- <span class="status-text" v-html="removesBranchText"> </span>
- <i v-tooltip :title="tooltipTitle" :aria-label="tooltipTitle" class="fa fa-question-circle">
+ <span class="status-text">
+ <gl-sprintf :message="$options.i18n.removesBranchText">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </span>
+ <i
+ v-tooltip
+ :title="$options.i18n.tooltipTitle"
+ :aria-label="$options.i18n.tooltipTitle"
+ class="fa fa-question-circle"
+ >
</i>
</p>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
index b6722de5277..f17e409d996 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
@@ -1,10 +1,10 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
},
props: {
commits: {
@@ -18,20 +18,20 @@ export default {
<template>
<div>
- <gl-dropdown
+ <gl-deprecated-dropdown
right
text="Use an existing commit message"
variant="link"
class="mr-commit-dropdown"
>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="commit in commits"
:key="commit.short_id"
class="text-nowrap text-truncate"
@click="$emit('input', commit.message)"
>
<span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }}
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-deprecated-dropdown-item>
+ </gl-deprecated-dropdown>
</div>
</template>
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 f02e0ac84da..12f65a4c235 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
@@ -1,6 +1,6 @@
<script>
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
-import Flash from '../../../flash';
+import { deprecatedCreateFlash as Flash } from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthor from '../mr_widget_author.vue';
import eventHub from '../../event_hub';
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 1a6e186a371..166700dbcbf 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>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlLoadingIcon } from '@gitlab/ui';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
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 c3cc30a1a6f..794c994bffe 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
@@ -4,7 +4,7 @@ import { escape } from 'lodash';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
-import Flash from '../../../flash';
+import { deprecatedCreateFlash as Flash } from '../../../flash';
import { __, sprintf } from '~/locale';
export default {
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 cc43135f50a..930a2b68d8e 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
@@ -8,14 +8,23 @@ import simplePoll from '~/lib/utils/simple_poll';
import { __, sprintf } from '~/locale';
import MergeRequest from '../../../merge_request';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import Flash from '../../../flash';
+import { deprecatedCreateFlash as Flash } from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
import SquashBeforeMerge from './squash_before_merge.vue';
import CommitsHeader from './commits_header.vue';
import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue';
-import { AUTO_MERGE_STRATEGIES } from '../../constants';
+import { AUTO_MERGE_STRATEGIES, DANGER, INFO, WARNING } from '../../constants';
+
+const PIPELINE_RUNNING_STATE = 'running';
+const PIPELINE_FAILED_STATE = 'failed';
+const PIPELINE_PENDING_STATE = 'pending';
+const PIPELINE_SUCCESS_STATE = 'success';
+
+const MERGE_FAILED_STATUS = 'failed';
+const MERGE_SUCCESS_STATUS = 'success';
+const MERGE_HOOK_VALIDATION_ERROR_STATUS = 'hook_validation_error';
export default {
name: 'ReadyToMerge',
@@ -29,6 +38,8 @@ export default {
GlSprintf,
GlLink,
GlDeprecatedButton,
+ MergeTrainHelperText: () =>
+ import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'),
MergeImmediatelyConfirmationDialog: () =>
import(
'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
@@ -60,35 +71,45 @@ export default {
const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr;
if ((hasCI && !ciStatus) || this.hasPipelineMustSucceedConflict) {
- return 'failed';
- } else if (this.isAutoMergeAvailable) {
- return 'pending';
- } else if (!pipeline) {
- return 'success';
- } else if (isPipelineFailed) {
- return 'failed';
+ return PIPELINE_FAILED_STATE;
+ }
+
+ if (this.isAutoMergeAvailable) {
+ return PIPELINE_PENDING_STATE;
+ }
+
+ if (pipeline && isPipelineFailed) {
+ return PIPELINE_FAILED_STATE;
}
- return 'success';
+ return PIPELINE_SUCCESS_STATE;
},
mergeButtonVariant() {
- if (this.status === 'failed') {
- return 'danger';
- } else if (this.status === 'pending') {
- return 'info';
+ if (this.status === PIPELINE_FAILED_STATE) {
+ return DANGER;
}
- return 'success';
+
+ if (this.status === PIPELINE_PENDING_STATE) {
+ return INFO;
+ }
+
+ return PIPELINE_SUCCESS_STATE;
},
iconClass() {
+ if (this.shouldRenderMergeTrainHelperText && !this.mr.preventMerge) {
+ return PIPELINE_RUNNING_STATE;
+ }
+
if (
- this.status === 'failed' ||
+ this.status === PIPELINE_FAILED_STATE ||
!this.commitMessage.length ||
!this.mr.isMergeAllowed ||
this.mr.preventMerge
) {
- return 'warning';
+ return WARNING;
}
- return 'success';
+
+ return PIPELINE_SUCCESS_STATE;
},
mergeButtonText() {
if (this.isMergingImmediately) {
@@ -167,11 +188,13 @@ export default {
.merge(options)
.then(res => res.data)
.then(data => {
- const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
+ const hasError =
+ data.status === MERGE_FAILED_STATUS ||
+ data.status === MERGE_HOOK_VALIDATION_ERROR_STATUS;
if (AUTO_MERGE_STRATEGIES.includes(data.status)) {
eventHub.$emit('MRWidgetUpdateRequested');
- } else if (data.status === 'success') {
+ } else if (data.status === MERGE_SUCCESS_STATUS) {
this.initiateMergePolling();
} else if (hasError) {
eventHub.$emit('FailedToMerge', data.merge_error);
@@ -269,7 +292,7 @@ export default {
<template>
<div>
- <div class="mr-widget-body media">
+ <div class="mr-widget-body media" :class="{ 'gl-pb-3': shouldRenderMergeTrainHelperText }">
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
@@ -358,6 +381,7 @@ export default {
<div
v-if="hasPipelineMustSucceedConflict"
class="gl-display-flex gl-align-items-center"
+ data-testid="pipeline-succeed-conflict"
>
<gl-sprintf :message="pipelineMustSucceedConflictText" />
<gl-link
@@ -379,6 +403,13 @@ export default {
</div>
</div>
</div>
+ <merge-train-helper-text
+ v-if="shouldRenderMergeTrainHelperText"
+ :pipeline-id="mr.pipeline.id"
+ :pipeline-link="mr.pipeline.path"
+ :merge-train-length="mr.mergeTrainsCount"
+ :merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath"
+ />
<template v-if="shouldShowMergeControls">
<div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message">
{{ __('Fast-forward merge without a merge commit') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index efd58341a2d..3cf7dc3c4d1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -38,7 +38,7 @@ export default {
<div class="inline">
<label
v-tooltip
- :class="{ 'gl-text-gray-600': isDisabled }"
+ :class="{ 'gl-text-gray-400': isDisabled }"
data-testid="squashLabel"
:data-title="tooltipTitle"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index d4a5fdb4b97..78e69b9ff9b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -1,10 +1,13 @@
<script>
+import { GlButton } from '@gitlab/ui';
import statusIcon from '../mr_widget_status_icon.vue';
+import notesEventHub from '~/notes/event_hub';
export default {
name: 'UnresolvedDiscussions',
components: {
statusIcon,
+ GlButton,
},
props: {
mr: {
@@ -12,23 +15,39 @@ export default {
required: true,
},
},
+ methods: {
+ jumpToFirstUnresolvedDiscussion() {
+ notesEventHub.$emit('jumpToFirstUnresolvedDiscussion');
+ },
+ },
};
</script>
<template>
- <div class="mr-widget-body media">
+ <div class="mr-widget-body media gl-flex-wrap">
<status-icon :show-disabled-button="true" status="warning" />
- <div class="media-body space-children">
- <span class="bold">
- {{ s__('mrWidget|There are unresolved threads. Please resolve these threads') }}
- </span>
- <a
+ <div class="media-body">
+ <span class="gl-ml-3 gl-font-weight-bold gl-display-block gl-w-100">{{
+ s__('mrWidget|Before this can be merged, one or more threads must be resolved.')
+ }}</span>
+ <gl-button
+ data-testid="jump-to-first"
+ class="gl-ml-3"
+ size="small"
+ icon="comment-next"
+ @click="jumpToFirstUnresolvedDiscussion"
+ >
+ {{ s__('mrWidget|Jump to first unresolved thread') }}
+ </gl-button>
+ <gl-button
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
- class="btn btn-default btn-sm js-create-issue"
+ class="js-create-issue gl-ml-3"
+ size="small"
+ icon="issue-new"
>
- {{ s__('mrWidget|Create an issue to resolve them later') }}
- </a>
+ {{ s__('mrWidget|Resolve all threads in new issue') }}
+ </gl-button>
</div>
</div>
</template>
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 118caac84b9..44668170fe4 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
@@ -1,8 +1,13 @@
<script>
import $ from 'jquery';
-import { GlDeprecatedButton } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
-import createFlash from '~/flash';
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+import getStateQuery from '../../queries/get_state.query.graphql';
+import workInProgressQuery from '../../queries/states/work_in_progress.query.graphql';
+import removeWipMutation from '../../queries/toggle_wip.mutation.graphql';
import StatusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
@@ -11,72 +16,151 @@ export default {
name: 'WorkInProgress',
components: {
StatusIcon,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
tooltip,
},
+ mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
+ apollo: {
+ userPermissions: {
+ query: workInProgressQuery,
+ skip() {
+ return !this.glFeatures.mergeRequestWidgetGraphql;
+ },
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: data => data.project.mergeRequest.userPermissions,
+ },
+ },
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() {
return {
+ userPermissions: {},
isMakingRequest: false,
};
},
computed: {
- wipInfoTooltip() {
- return s__(
- 'mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged',
- );
+ canUpdate() {
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ return this.userPermissions.updateMergeRequest;
+ }
+
+ return Boolean(this.mr.removeWIPPath);
},
},
methods: {
- handleRemoveWIP() {
+ removeWipMutation() {
this.isMakingRequest = true;
- this.service
- .removeWIP()
- .then(res => res.data)
- .then(data => {
- eventHub.$emit('UpdateWidgetData', data);
+
+ this.$apollo
+ .mutate({
+ mutation: removeWipMutation,
+ variables: {
+ ...this.mergeRequestQueryVariables,
+ wip: false,
+ },
+ update(
+ store,
+ {
+ data: {
+ mergeRequestSetWip: {
+ errors,
+ mergeRequest: { workInProgress, title },
+ },
+ },
+ },
+ ) {
+ if (errors?.length) {
+ createFlash(__('Something went wrong. Please try again.'));
+
+ return;
+ }
+
+ const data = store.readQuery({
+ query: getStateQuery,
+ variables: this.mergeRequestQueryVariables,
+ });
+ data.project.mergeRequest.workInProgress = workInProgress;
+ data.project.mergeRequest.title = title;
+ store.writeQuery({
+ query: getStateQuery,
+ data,
+ variables: this.mergeRequestQueryVariables,
+ });
+ },
+ optimisticResponse: {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Mutation',
+ mergeRequestSetWip: {
+ __typename: 'MergeRequestSetWipPayload',
+ errors: [],
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ title: this.mr.title,
+ workInProgress: false,
+ },
+ },
+ },
+ })
+ .then(({ data: { mergeRequestSetWip: { mergeRequest: { title } } } }) => {
createFlash(__('The merge request can now be merged.'), 'notice');
- $('.merge-request .detail-page-description .title').text(this.mr.title);
+ $('.merge-request .detail-page-description .title').text(title);
})
- .catch(() => {
+ .catch(() => createFlash(__('Something went wrong. Please try again.')))
+ .finally(() => {
this.isMakingRequest = false;
- createFlash(__('Something went wrong. Please try again.'));
});
},
+ handleRemoveWIP() {
+ if (this.glFeatures.mergeRequestWidgetGraphql) {
+ this.removeWipMutation();
+ } else {
+ this.isMakingRequest = true;
+ this.service
+ .removeWIP()
+ .then(res => res.data)
+ .then(data => {
+ eventHub.$emit('UpdateWidgetData', data);
+ createFlash(__('The merge request can now be merged.'), 'notice');
+ $('.merge-request .detail-page-description .title').text(this.mr.title);
+ })
+ .catch(() => {
+ this.isMakingRequest = false;
+ createFlash(__('Something went wrong. Please try again.'));
+ });
+ }
+ },
},
};
</script>
<template>
<div class="mr-widget-body media">
- <status-icon :show-disabled-button="Boolean(mr.removeWIPPath)" status="warning" />
- <div class="media-body space-children">
- <span class="bold">
- {{ __('This is a Work in Progress') }}
- <i
- v-tooltip
- class="fa fa-question-circle"
- :title="wipInfoTooltip"
- :aria-label="wipInfoTooltip"
- >
- </i>
- </span>
- <gl-deprecated-button
- v-if="mr.removeWIPPath"
- size="sm"
- variant="default"
+ <status-icon :show-disabled-button="canUpdate" status="warning" />
+ <div class="media-body">
+ <div class="gl-ml-3 float-left">
+ <span class="gl-font-weight-bold">
+ {{ __('This merge request is still a work in progress.') }}
+ </span>
+ <span class="gl-display-block text-muted">{{
+ __("Draft merge requests can't be merged.")
+ }}</span>
+ </div>
+ <gl-button
+ v-if="canUpdate"
+ size="small"
:disabled="isMakingRequest"
:loading="isMakingRequest"
- class="js-remove-wip"
+ class="js-remove-wip gl-ml-3"
@click="handleRemoveWIP"
>
- {{ s__('mrWidget|Resolve WIP status') }}
- </gl-deprecated-button>
+ {{ s__('mrWidget|Mark as ready') }}
+ </gl-button>
</div>
</div>
</template>
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
index f6e21dc1ec1..c7d9453a5c9 100644
--- 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
@@ -1,6 +1,6 @@
<script>
-import { n__ } from '~/locale';
import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
+import { n__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
import Poll from '~/lib/utils/poll';
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
index dc16d46dd8e..b74e036d9d9 100644
--- 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
@@ -1,6 +1,6 @@
<script>
-import { s__ } from '~/locale';
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
export default {
name: 'TerraformPlan',
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 1002bb728a0..77dfbf9d385 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -1,6 +1,9 @@
+import { s__ } from '~/locale';
+
export const SUCCESS = 'success';
export const WARNING = 'warning';
export const DANGER = 'danger';
+export const INFO = 'info';
export const WARNING_MESSAGE_CLASS = 'warning_message';
export const DANGER_MESSAGE_CLASS = 'danger_message';
@@ -10,3 +13,15 @@ export const MTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds';
export const MT_MERGE_STRATEGY = 'merge_train';
export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY];
+
+// SP - "Suggest Pipelines"
+export const SP_TRACK_LABEL = 'no_pipeline_noticed';
+export const SP_LINK_TRACK_EVENT = 'click_link';
+export const SP_SHOW_TRACK_EVENT = 'click_button';
+export const SP_LINK_TRACK_VALUE = 30;
+export const SP_SHOW_TRACK_VALUE = 10;
+export const SP_HELP_CONTENT = s__(
+ `mrWidget|Use %{linkStart}CI pipelines to test your code%{linkEnd} by simply adding a GitLab CI configuration file to your project. It only takes a minute to make your code more secure and robust.`,
+);
+export const SP_HELP_URL = 'https://about.gitlab.com/blog/2019/07/12/guide-to-ci-cd-pipelines/';
+export const SP_ICON_NAME = 'status_notfound';
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 068829912bf..87e56dfcbdf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
-import Translate from '../vue_shared/translate';
import VueApollo from 'vue-apollo';
+import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
Vue.use(Translate);
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/merge_request_query_variables.js b/app/assets/javascripts/vue_merge_request_widget/mixins/merge_request_query_variables.js
new file mode 100644
index 00000000000..3a121908f36
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/merge_request_query_variables.js
@@ -0,0 +1,10 @@
+export default {
+ computed: {
+ mergeRequestQueryVariables() {
+ return {
+ projectPath: this.mr.targetProjectFullPath,
+ iid: `${this.mr.iid}`,
+ };
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index 319b6c333f4..dc8a6b56d58 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -32,5 +32,8 @@ export default {
isMergeImmediatelyDangerous() {
return false;
},
+ shouldRenderMergeTrainHelperText() {
+ return false;
+ },
},
};
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 cff85fe232d..36a883869f1 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
@@ -7,7 +7,8 @@ import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
-import createFlash from '../flash';
+import { deprecatedCreateFlash as createFlash } from '../flash';
+import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import Loading from './components/loading.vue';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetSuggestPipeline from './components/mr_widget_suggest_pipeline.vue';
@@ -42,6 +43,7 @@ import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
import { setFaviconOverlay } from '../lib/utils/common_utils';
import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
+import getStateQuery from './queries/get_state.query.graphql';
export default {
el: '#js-vue-mr-widget',
@@ -83,6 +85,27 @@ export default {
GroupedAccessibilityReportsApp,
MrWidgetApprovals,
},
+ apollo: {
+ state: {
+ query: getStateQuery,
+ manual: true,
+ pollInterval: 10 * 1000,
+ skip() {
+ return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql;
+ },
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ result({
+ data: {
+ project: { mergeRequest },
+ },
+ }) {
+ this.mr.setGraphqlData(mergeRequest);
+ },
+ },
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
props: {
mrData: {
type: Object,
@@ -116,7 +139,12 @@ export default {
return this.mr.hasCI || this.hasPipelineMustSucceedConflict;
},
shouldSuggestPipelines() {
- return gon.features?.suggestPipeline && !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath;
+ return (
+ gon.features?.suggestPipeline &&
+ !this.mr.hasCI &&
+ this.mr.mergeRequestAddCiConfigPath &&
+ !this.mr.isDismissedSuggestPipeline
+ );
},
shouldRenderCodeQuality() {
return this.mr?.codeclimate?.head_path;
@@ -374,6 +402,9 @@ export default {
this.stopPolling();
});
},
+ dismissSuggestPipelines() {
+ this.mr.isDismissedSuggestPipeline = true;
+ },
},
};
</script>
@@ -382,10 +413,14 @@ export default {
<mr-widget-header :mr="mr" />
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
+ data-testid="mr-suggest-pipeline"
class="mr-widget-workflow"
:pipeline-path="mr.mergeRequestAddCiConfigPath"
:pipeline-svg-path="mr.pipelinesEmptySvgPath"
:human-access="mr.humanAccess.toLowerCase()"
+ :user-callouts-path="mr.userCalloutsPath"
+ :user-callout-feature-id="mr.suggestPipelineFeatureId"
+ @dismiss="dismissSuggestPipelines"
/>
<mr-widget-pipeline-container
v-if="shouldRenderPipelines"
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
new file mode 100644
index 00000000000..488397e7735
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
@@ -0,0 +1,8 @@
+query getState($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ mergeRequest(iid: $iid) {
+ title
+ workInProgress
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql
new file mode 100644
index 00000000000..73e205ebf2b
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/work_in_progress.query.graphql
@@ -0,0 +1,9 @@
+query workInProgressQuery($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ mergeRequest(iid: $iid) {
+ userPermissions {
+ updateMergeRequest
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql
new file mode 100644
index 00000000000..37abe5ddf3c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/toggle_wip.mutation.graphql
@@ -0,0 +1,9 @@
+mutation toggleWIPStatus($projectPath: ID!, $iid: String!, $wip: Boolean!) {
+ mergeRequestSetWip(input: { projectPath: $projectPath, iid: $iid, wip: $wip }) {
+ mergeRequest {
+ title
+ workInProgress
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js
index 3648db795f5..419793977f0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js
@@ -69,6 +69,3 @@ export const receiveArtifactsSuccess = ({ commit }, response) => {
};
export const receiveArtifactsError = ({ commit }) => commit(types.RECEIVE_ARTIFACTS_ERROR);
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js
index 8921637b93b..5215d210e1c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/getters.js
@@ -1,5 +1,6 @@
import { s__, n__ } from '~/locale';
+// eslint-disable-next-line import/prefer-default-export
export const title = state => {
if (state.isLoading) {
return s__('BuildArtifacts|Loading artifacts');
@@ -11,6 +12,3 @@ export const title = state => {
return n__('View exposed artifact', 'View %d exposed artifacts', state.artifacts.length);
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
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 44e8167d6a3..3bd512c89bf 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,21 +1,21 @@
import { stateKey } from './state_maps';
-export default function deviseState(data) {
- if (data.project_archived) {
+export default function deviseState() {
+ if (this.projectArchived) {
return stateKey.archived;
- } else if (data.branch_missing) {
+ } else if (this.branchMissing) {
return stateKey.missingBranch;
- } else if (!data.commits_count) {
+ } else if (!this.commitsCount) {
return stateKey.nothingToMerge;
} else if (this.mergeStatus === 'unchecked' || this.mergeStatus === 'checking') {
return stateKey.checking;
- } else if (data.has_conflicts) {
+ } else if (this.hasConflicts) {
return stateKey.conflicts;
} else if (this.shouldBeRebased) {
return stateKey.rebase;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return stateKey.pipelineFailed;
- } else if (data.work_in_progress) {
+ } else if (this.workInProgress) {
return stateKey.workInProgress;
} else if (this.hasMergeableDiscussionsState) {
return stateKey.unresolvedDiscussions;
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 8b9799d9775..8c98ba1b023 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
@@ -60,6 +60,9 @@ export default class MergeRequestStore {
this.rebaseInProgress = data.rebase_in_progress;
this.mergeRequestDiffsPath = data.diffs_path;
this.approvalsWidgetType = data.approvals_widget_type;
+ this.projectArchived = data.project_archived;
+ this.branchMissing = data.branch_missing;
+ this.hasConflicts = data.has_conflicts;
if (data.issues_links) {
const links = data.issues_links;
@@ -90,7 +93,8 @@ export default class MergeRequestStore {
this.ffOnlyEnabled = data.ff_only_enabled;
this.shouldBeRebased = Boolean(data.should_be_rebased);
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
- this.isOpen = data.state === 'opened';
+ this.mergeRequestState = data.state;
+ this.isOpen = this.mergeRequestState === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.latestSHA = data.diff_head_sha;
@@ -133,6 +137,10 @@ export default class MergeRequestStore {
this.mergeCommitPath = data.merge_commit_path;
this.canPushToSourceBranch = data.can_push_to_source_branch;
+ if (data.work_in_progress !== undefined) {
+ this.workInProgress = data.work_in_progress;
+ }
+
const currentUser = data.current_user;
this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path;
@@ -143,19 +151,25 @@ export default class MergeRequestStore {
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
- this.setState(data);
+ this.setState();
+ }
+
+ setGraphqlData(data) {
+ this.workInProgress = data.workInProgress;
+
+ this.setState();
}
- setState(data) {
+ setState() {
if (this.mergeOngoing) {
this.state = 'merging';
return;
}
if (this.isOpen) {
- this.state = getStateKey.call(this, data);
+ this.state = getStateKey.call(this);
} else {
- switch (data.state) {
+ switch (this.mergeRequestState) {
case 'merged':
this.state = 'merged';
break;
@@ -194,6 +208,9 @@ export default class MergeRequestStore {
this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path;
this.humanAccess = data.human_access;
this.newPipelinePath = data.new_project_pipeline_path;
+ this.userCalloutsPath = data.user_callouts_path;
+ this.suggestPipelineFeatureId = data.suggest_pipeline_feature_id;
+ this.isDismissedSuggestPipeline = data.is_dismissed_suggest_pipeline;
// codeclimate
const blobPath = data.blob_path || {};
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
index 27f1a4f75d5..9e2b3097499 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
@@ -1,3 +1,10 @@
+import {
+ SNIPPET_MARK_VIEW_APP_START,
+ SNIPPET_MARK_BLOBS_CONTENT,
+ SNIPPET_MEASURE_BLOBS_CONTENT,
+ SNIPPET_MEASURE_BLOBS_CONTENT_WITHIN_APP,
+} from '~/performance_constants';
+
export default {
props: {
content: {
@@ -9,4 +16,13 @@ export default {
required: true,
},
},
+ mounted() {
+ window.requestAnimationFrame(() => {
+ if (!performance.getEntriesByName(SNIPPET_MARK_BLOBS_CONTENT).length) {
+ performance.mark(SNIPPET_MARK_BLOBS_CONTENT);
+ performance.measure(SNIPPET_MEASURE_BLOBS_CONTENT);
+ performance.measure(SNIPPET_MEASURE_BLOBS_CONTENT_WITHIN_APP, SNIPPET_MARK_VIEW_APP_START);
+ }
+ });
+ },
};
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index 1eb05780206..55a6267f9ff 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -1,6 +1,6 @@
<script>
-import ViewerMixin from './mixins';
import { GlIcon } from '@gitlab/ui';
+import ViewerMixin from './mixins';
import { HIGHLIGHT_CLASS_NAME } from './constants';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
index ac95c88225e..6f5ea8dcbee 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
@@ -45,7 +45,7 @@ export default {
};
</script>
<template>
- <gl-new-dropdown :text="$options.labels.defaultLabel" category="primary" variant="info">
+ <gl-new-dropdown right :text="$options.labels.defaultLabel" category="primary" variant="info">
<div class="pb-2 mx-1">
<template v-if="sshLink">
<gl-new-dropdown-header>{{ $options.labels.ssh }}</gl-new-dropdown-header>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_modal.vue
index 52ff906ccec..e7f6cc1abc0 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlModal } from '@gitlab/ui';
-import csrf from '~/lib/utils/csrf';
import { uniqueId } from 'lodash';
+import csrf from '~/lib/utils/csrf';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index 47231c4ad39..3bf629d4acb 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -39,7 +39,7 @@ export default {
<template>
<div class="file-container">
<div class="file-content">
- <p class="prepend-top-10 file-info">
+ <p class="gl-mt-3 file-info">
{{ fileName }}
<template v-if="fileSize > 0">
({{ fileSizeReadable }})
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 1344c766e0e..f9b678e33cd 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -3,9 +3,9 @@ import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { GlSkeletonLoading } from '@gitlab/ui';
+import { forEach, escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import { forEach, escape } from 'lodash';
const { CancelToken } = axios;
let axiosSource;
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
index ddbb474bab6..3b6b0a91e97 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
@@ -1,5 +1,11 @@
<script>
-import { GlIcon, GlDeprecatedButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
+import {
+ GlIcon,
+ GlButton,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
+ GlFormGroup,
+} from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
@@ -22,9 +28,9 @@ const events = {
export default {
components: {
GlIcon,
- GlDeprecatedButton,
- GlDropdown,
- GlDropdownItem,
+ GlButton,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
GlFormGroup,
TooltipOnTruncate,
DateTimePickerInput,
@@ -206,7 +212,8 @@ export default {
placement="top"
class="d-inline-block"
>
- <gl-dropdown
+ <gl-deprecated-dropdown
+ ref="dropdown"
:text="timeWindowText"
v-bind="$attrs"
class="date-time-picker w-100"
@@ -215,7 +222,9 @@ export default {
>
<template #button-content>
<span class="gl-flex-grow-1 text-truncate">{{ timeWindowText }}</span>
- <span v-if="utc" class="text-muted gl-font-weight-bold gl-font-sm">{{ __('UTC') }}</span>
+ <span v-if="utc" class="gl-text-gray-500 gl-font-weight-bold gl-font-sm">{{
+ __('UTC')
+ }}</span>
<gl-icon class="gl-dropdown-caret" name="chevron-down" aria-hidden="true" />
</template>
@@ -242,10 +251,17 @@ export default {
/>
</div>
<gl-form-group>
- <gl-deprecated-button @click="closeDropdown">{{ __('Cancel') }}</gl-deprecated-button>
- <gl-deprecated-button variant="success" :disabled="!isValid" @click="setFixedRange()">
+ <gl-button data-testid="cancelButton" @click="closeDropdown">{{
+ __('Cancel')
+ }}</gl-button>
+ <gl-button
+ variant="success"
+ category="primary"
+ :disabled="!isValid"
+ @click="setFixedRange()"
+ >
{{ __('Apply') }}
- </gl-deprecated-button>
+ </gl-button>
</gl-form-group>
</gl-form-group>
<gl-form-group
@@ -256,7 +272,7 @@ export default {
<span class="gl-pl-5-deprecated-no-really-do-not-use-me">{{ __('Quick range') }}</span>
</template>
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
v-for="(option, index) in options"
:key="index"
data-qa-selector="quick_range_item"
@@ -270,9 +286,9 @@ export default {
:class="{ invisible: !isOptionActive(option) }"
/>
{{ option.label }}
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
</gl-form-group>
</div>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</tooltip-on-truncate>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_container.vue b/app/assets/javascripts/vue_shared/components/dismissible_container.vue
new file mode 100644
index 00000000000..b4227bae09e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dismissible_container.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ props: {
+ path: {
+ type: String,
+ required: true,
+ },
+ featureId: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ dismiss() {
+ axios
+ .post(this.path, {
+ feature_name: this.featureId,
+ })
+ .catch(e => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings, no-console
+ console.error('Failed to dismiss message.', e);
+ });
+
+ this.$emit('dismiss');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-display-flex gl-align-items-center">
+ <slot name="title"></slot>
+ <div class="ml-auto">
+ <button
+ :aria-label="__('Close')"
+ class="btn-blank"
+ type="button"
+ data-testid="close"
+ @click="dismiss"
+ >
+ <gl-icon name="close" aria-hidden="true" class="gl-text-gray-500" />
+ </button>
+ </div>
+ </div>
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
new file mode 100644
index 00000000000..c7d7c3a1d24
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dismissible_feedback_alert.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import { slugifyWithUnderscore } from '~/lib/utils/text_utility';
+
+export default {
+ components: {
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ LocalStorageSync,
+ },
+ props: {
+ featureName: {
+ type: String,
+ required: true,
+ },
+ feedbackLink: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isDismissed: 'false',
+ };
+ },
+ computed: {
+ storageKey() {
+ return `${slugifyWithUnderscore(this.featureName)}_feedback_dismissed`;
+ },
+ showAlert() {
+ return this.isDismissed === 'false';
+ },
+ },
+ methods: {
+ dismissFeedbackAlert() {
+ this.isDismissed = 'true';
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-show="showAlert">
+ <local-storage-sync
+ :value="isDismissed"
+ :storage-key="storageKey"
+ @input="dismissFeedbackAlert"
+ />
+ <gl-alert v-if="showAlert" class="gl-mt-5" @dismiss="dismissFeedbackAlert">
+ <gl-sprintf
+ :message="
+ __(
+ 'We’ve been making changes to %{featureName} and we’d love your feedback %{linkStart}in this issue%{linkEnd} to help us improve the experience.',
+ )
+ "
+ >
+ <template #featureName>{{ featureName }}</template>
+ <template #link="{ content }">
+ <gl-link :href="feedbackLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index 1f904cd3c6c..546ee56355f 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -1,7 +1,6 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import Icon from '~/vue_shared/components/icon.vue';
/**
* Port of detail_behavior expand button.
@@ -16,8 +15,7 @@ import Icon from '~/vue_shared/components/icon.vue';
export default {
name: 'ExpandButton',
components: {
- GlDeprecatedButton,
- Icon,
+ GlButton,
},
data() {
return {
@@ -41,25 +39,23 @@ export default {
</script>
<template>
<span>
- <gl-deprecated-button
+ <gl-button
v-show="isCollapsed"
:aria-label="ariaLabel"
type="button"
class="js-text-expander-prepend text-expander btn-blank"
+ icon="ellipsis_h"
@click="onClick"
- >
- <icon :size="12" name="ellipsis_h" />
- </gl-deprecated-button>
+ />
<span v-if="isCollapsed"> <slot name="short"></slot> </span>
<span v-if="!isCollapsed"> <slot name="expanded"></slot> </span>
- <gl-deprecated-button
+ <gl-button
v-show="!isCollapsed"
:aria-label="ariaLabel"
type="button"
class="js-text-expander-append text-expander btn-blank"
+ icon="ellipsis_h"
@click="onClick"
- >
- <icon :size="12" name="ellipsis_h" />
- </gl-deprecated-button>
+ />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 7484486d6b4..6190b07962d 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -87,7 +87,7 @@ export default {
<span>
<gl-loading-icon v-if="loading" :inline="true" />
<gl-icon v-else-if="isSymlink" name="symlink" :size="size" />
- <svg v-else-if="!folder" :class="[iconSizeClass, cssClasses]">
+ <svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]">
<use v-bind="{ 'xlink:href': spriteHref }" />
</svg>
<gl-icon v-else :name="folderIconName" :size="size" class="folder-icon" />
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index 9ecae87c1a9..b70f093e930 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -586,5 +586,16 @@ const fileNameIcons = {
};
export default function getIconForFile(name) {
- return fileNameIcons[name] || fileExtensionIcons[name ? name.split('.').pop() : ''] || '';
+ return (
+ fileNameIcons[name] ||
+ fileExtensionIcons[
+ name
+ ? name
+ .split('.')
+ .pop()
+ .toLowerCase()
+ : ''
+ ] ||
+ ''
+ );
}
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 6665a5754b3..7b3d1d0afd6 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
@@ -1,8 +1,23 @@
+import { __ } from '~/locale';
+
export const ANY_AUTHOR = 'Any';
+export const NO_LABEL = 'No label';
+
export const DEBOUNCE_DELAY = 200;
export const SortDirection = {
descending: 'descending',
ascending: 'ascending',
};
+
+export const defaultMilestones = [
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ { value: 'None', text: __('None') },
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ { value: 'Any', text: __('Any') },
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ { value: 'Upcoming', text: __('Upcoming') },
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ { value: 'Started', text: __('Started') },
+];
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 04090213218..ee293d37b66 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
@@ -8,13 +8,14 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
+import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
-import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
+import { stripQuotes } from './filtered_search_utils';
import { SortDirection } from './constants';
export default {
@@ -44,7 +45,8 @@ export default {
},
sortOptions: {
type: Array,
- required: true,
+ default: () => [],
+ required: false,
},
initialFilterValue: {
type: Array,
@@ -63,7 +65,7 @@ export default {
},
},
data() {
- let selectedSortOption = this.sortOptions[0].sortDirection.descending;
+ let selectedSortOption = this.sortOptions[0]?.sortDirection?.descending;
let selectedSortDirection = SortDirection.descending;
// Extract correct sortBy value based on initialSortBy
@@ -118,6 +120,11 @@ export default {
? __('Sort direction: Ascending')
: __('Sort direction: Descending');
},
+ filteredRecentSearches() {
+ return this.recentSearchesStorageKey
+ ? this.recentSearches.filter(item => typeof item !== 'string')
+ : undefined;
+ },
},
watch: {
/**
@@ -184,6 +191,41 @@ export default {
this.recentSearches = resultantSearches;
});
},
+ /**
+ * When user hits Enter/Return key while typing tokens, we emit `onFilter`
+ * event immediately so at that time, we don't want to keep tokens dropdown
+ * visible on UI so this is essentially a hack which allows us to do that
+ * until `GlFilteredSearch` natively supports this.
+ * See this discussion https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36421#note_385729546
+ */
+ blurSearchInput() {
+ const searchInputEl = this.$refs.filteredSearchInput.$el.querySelector(
+ '.gl-filtered-search-token-segment-input',
+ );
+ if (searchInputEl) {
+ searchInputEl.blur();
+ }
+ },
+ /**
+ * This method removes quotes enclosure from filter values which are
+ * done by `GlFilteredSearch` internally when filter value contains
+ * spaces.
+ */
+ removeQuotesEnclosure(filters = []) {
+ return filters.map(filter => {
+ if (typeof filter === 'object') {
+ const valueString = filter.value.data;
+ return {
+ ...filter,
+ value: {
+ data: stripQuotes(valueString),
+ operator: filter.value.operator,
+ },
+ };
+ }
+ return filter;
+ });
+ },
handleSortOptionClick(sortBy) {
this.selectedSortOption = sortBy;
this.$emit('onSort', sortBy.sortDirection[this.selectedSortDirection]);
@@ -196,7 +238,7 @@ export default {
this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]);
},
handleHistoryItemSelected(filters) {
- this.$emit('onFilter', filters);
+ this.$emit('onFilter', this.removeQuotesEnclosure(filters));
},
handleClearHistory() {
const resultantSearches = this.recentSearchesStore.setRecentSearches([]);
@@ -217,7 +259,8 @@ export default {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
});
}
- this.$emit('onFilter', filters);
+ this.blurSearchInput();
+ this.$emit('onFilter', this.removeQuotesEnclosure(filters));
},
},
};
@@ -226,10 +269,11 @@ export default {
<template>
<div class="vue-filtered-search-bar-container d-md-flex">
<gl-filtered-search
+ ref="filteredSearchInput"
v-model="filterValue"
:placeholder="searchInputPlaceholder"
:available-tokens="tokens"
- :history-items="recentSearches"
+ :history-items="filteredRecentSearches"
class="flex-grow-1"
@history-item-selected="handleHistoryItemSelected"
@clear-history="handleClearHistory"
@@ -238,7 +282,7 @@ export default {
<template #history-item="{ historyItem }">
<template v-for="(token, index) in historyItem">
<span v-if="typeof token === 'string'" :key="index" class="gl-px-1">"{{ token }}"</span>
- <span v-else :key="`${token.type}-${token.value.data}`" class="gl-px-1">
+ <span v-else :key="`${index}-${token.type}-${token.value.data}`" class="gl-px-1">
<span v-if="tokenTitles[token.type]"
>{{ tokenTitles[token.type] }} :{{ token.value.operator }}</span
>
@@ -247,7 +291,7 @@ export default {
</template>
</template>
</gl-filtered-search>
- <gl-button-group class="sort-dropdown-container d-flex">
+ <gl-button-group v-if="selectedSortOption" class="sort-dropdown-container d-flex">
<gl-dropdown :text="selectedSortOption.title" :right="true" class="w-100">
<gl-dropdown-item
v-for="sortBy in sortOptions"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
new file mode 100644
index 00000000000..85f7f746b49
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
@@ -0,0 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
+export const stripQuotes = value => {
+ return value.includes(' ') ? value.slice(1, -1) : value;
+};
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 d50649d2581..969e914ef0c 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
@@ -3,12 +3,12 @@ import {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
+ GlDeprecatedDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import { ANY_AUTHOR, DEBOUNCE_DELAY } from '../constants';
@@ -19,7 +19,7 @@ export default {
GlFilteredSearchToken,
GlAvatar,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
+ GlDeprecatedDropdownDivider,
GlLoadingIcon,
},
props: {
@@ -102,7 +102,7 @@ export default {
<gl-filtered-search-suggestion :value="$options.anyAuthor">
{{ __('Any') }}
</gl-filtered-search-suggestion>
- <gl-dropdown-divider />
+ <gl-deprecated-dropdown-divider />
<gl-loading-icon v-if="loading" />
<template v-else>
<gl-filtered-search-suggestion
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
new file mode 100644
index 00000000000..726a1c49993
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -0,0 +1,126 @@
+<script>
+import {
+ GlToken,
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlNewDropdownDivider as GlDropdownDivider,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { __ } from '~/locale';
+
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+import { stripQuotes } from '../filtered_search_utils';
+import { NO_LABEL, DEBOUNCE_DELAY } from '../constants';
+
+export default {
+ noLabel: NO_LABEL,
+ components: {
+ GlToken,
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlLoadingIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ labels: this.config.initialLabels || [],
+ loading: true,
+ };
+ },
+ computed: {
+ currentValue() {
+ return this.value.data.toLowerCase();
+ },
+ activeLabel() {
+ return this.labels.find(
+ label => label.title.toLowerCase() === stripQuotes(this.currentValue),
+ );
+ },
+ containerStyle() {
+ if (this.activeLabel) {
+ const { color, textColor } = convertObjectPropsToCamelCase(this.activeLabel);
+
+ return { backgroundColor: color, color: textColor };
+ }
+ return {};
+ },
+ },
+ watch: {
+ active: {
+ immediate: true,
+ handler(newValue) {
+ if (!newValue && !this.labels.length) {
+ this.fetchLabelBySearchTerm(this.value.data);
+ }
+ },
+ },
+ },
+ methods: {
+ fetchLabelBySearchTerm(searchTerm) {
+ this.loading = true;
+ this.config
+ .fetchLabels(searchTerm)
+ .then(res => {
+ // We'd want to avoid doing this check but
+ // labels.json and /groups/:id/labels & /projects/:id/labels
+ // return response differently.
+ this.labels = Array.isArray(res) ? res : res.data;
+ })
+ .catch(() => createFlash(__('There was a problem fetching labels.')))
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ searchLabels: debounce(function debouncedSearch({ data }) {
+ this.fetchLabelBySearchTerm(data);
+ }, DEBOUNCE_DELAY),
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="searchLabels"
+ >
+ <template #view-token="{ inputValue, cssClasses, listeners }">
+ <gl-token variant="search-value" :class="cssClasses" :style="containerStyle" v-on="listeners"
+ >~{{ activeLabel ? activeLabel.title : inputValue }}</gl-token
+ >
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion :value="$options.noLabel">{{
+ __('No label')
+ }}</gl-filtered-search-suggestion>
+ <gl-dropdown-divider />
+ <gl-loading-icon v-if="loading" />
+ <template v-else>
+ <gl-filtered-search-suggestion v-for="label in labels" :key="label.id" :value="label.title">
+ <div class="gl-display-flex">
+ <span
+ :style="{ backgroundColor: label.color }"
+ class="gl-display-inline-block mr-2 p-2"
+ ></span>
+ <div>{{ label.title }}</div>
+ </div>
+ </gl-filtered-search-suggestion>
+ </template>
+ </template>
+ </gl-filtered-search-token>
+</template>
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
new file mode 100644
index 00000000000..cf1ac4e718b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -0,0 +1,110 @@
+<script>
+import {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlNewDropdownDivider as GlDropdownDivider,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+
+import { stripQuotes } from '../filtered_search_utils';
+import { defaultMilestones, DEBOUNCE_DELAY } from '../constants';
+
+export default {
+ defaultMilestones,
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlLoadingIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ milestones: this.config.initialMilestones || [],
+ loading: true,
+ };
+ },
+ computed: {
+ currentValue() {
+ return this.value.data.toLowerCase();
+ },
+ activeMilestone() {
+ return this.milestones.find(
+ milestone => milestone.title.toLowerCase() === stripQuotes(this.currentValue),
+ );
+ },
+ },
+ watch: {
+ active: {
+ immediate: true,
+ handler(newValue) {
+ if (!newValue && !this.milestones.length) {
+ this.fetchMilestoneBySearchTerm(this.value.data);
+ }
+ },
+ },
+ },
+ methods: {
+ fetchMilestoneBySearchTerm(searchTerm = '') {
+ this.loading = true;
+ this.config
+ .fetchMilestones(searchTerm)
+ .then(({ data }) => {
+ this.milestones = data;
+ })
+ .catch(() => createFlash(__('There was a problem fetching milestones.')))
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ searchMilestones: debounce(function debouncedSearch({ data }) {
+ this.fetchMilestoneBySearchTerm(data);
+ }, DEBOUNCE_DELAY),
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="searchMilestones"
+ >
+ <template #view="{ inputValue }">
+ <span>%{{ activeMilestone ? activeMilestone.title : inputValue }}</span>
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="milestone in $options.defaultMilestones"
+ :key="milestone.value"
+ :value="milestone.value"
+ >{{ milestone.text }}</gl-filtered-search-suggestion
+ >
+ <gl-dropdown-divider />
+ <gl-loading-icon v-if="loading" />
+ <template v-else>
+ <gl-filtered-search-suggestion
+ v-for="milestone in milestones"
+ :key="milestone.id"
+ :value="milestone.title"
+ >
+ <div>{{ milestone.title }}</div>
+ </gl-filtered-search-suggestion>
+ </template>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
index 0ef4f1eda27..00bc46257ed 100644
--- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
@@ -5,39 +5,102 @@ import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils';
import SidebarMediator from '~/sidebar/sidebar_mediator';
-/**
- * Creates the HTML template for each row of the mentions dropdown.
- *
- * @param original - An object from the array returned from the `autocomplete_sources/members` API
- * @returns {string} - An HTML template
- */
-function menuItemTemplate({ original }) {
- const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
-
- const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
- gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
-
- const avatarTag = original.avatar_url
- ? `<img
- src="${original.avatar_url}"
- alt="${original.username}'s avatar"
- class="${avatarClasses}"/>`
- : `<div class="${avatarClasses}">${original.username.charAt(0).toUpperCase()}</div>`;
-
- const name = escape(original.name);
-
- const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
-
- const icon = original.mentionsDisabled
- ? spriteIcon('notifications-off', 's16 gl-vertical-align-middle gl-ml-3')
- : '';
-
- return `${avatarTag}
- ${original.username}
- <small class="gl-text-small gl-font-weight-normal gl-reset-color">${name}${count}</small>
- ${icon}`;
+const AutoComplete = {
+ Issues: 'issues',
+ Labels: 'labels',
+ Members: 'members',
+};
+
+function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
+ const currentLineNumber = fullText.slice(0, selectionStart).split('\n').length;
+ const currentLine = fullText.split('\n')[currentLineNumber - 1];
+ return currentLine.startsWith(searchString);
}
+const autoCompleteMap = {
+ [AutoComplete.Issues]: {
+ filterValues() {
+ return this[AutoComplete.Issues];
+ },
+ menuItemTemplate({ original }) {
+ return `<small>${original.reference || original.iid}</small> ${escape(original.title)}`;
+ },
+ },
+ [AutoComplete.Labels]: {
+ filterValues() {
+ const fullText = this.$slots.default?.[0]?.elm?.value;
+ const selectionStart = this.$slots.default?.[0]?.elm?.selectionStart;
+
+ if (doesCurrentLineStartWith('/label', fullText, selectionStart)) {
+ return this.labels.filter(label => !label.set);
+ }
+
+ if (doesCurrentLineStartWith('/unlabel', fullText, selectionStart)) {
+ return this.labels.filter(label => label.set);
+ }
+
+ return this.labels;
+ },
+ menuItemTemplate({ original }) {
+ return `
+ <span class="dropdown-label-box" style="background: ${escape(original.color)};"></span>
+ ${escape(original.title)}`;
+ },
+ },
+ [AutoComplete.Members]: {
+ filterValues() {
+ const fullText = this.$slots.default?.[0]?.elm?.value;
+ const selectionStart = this.$slots.default?.[0]?.elm?.selectionStart;
+
+ // Need to check whether sidebar store assignees has been updated
+ // in the case where the assignees AJAX response comes after the user does @ autocomplete
+ const isAssigneesLengthSame =
+ this.assignees?.length === SidebarMediator.singleton?.store?.assignees?.length;
+
+ if (!this.assignees || !isAssigneesLengthSame) {
+ this.assignees =
+ SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
+ }
+
+ if (doesCurrentLineStartWith('/assign', fullText, selectionStart)) {
+ return this.members.filter(member => !this.assignees.includes(member.username));
+ }
+
+ if (doesCurrentLineStartWith('/unassign', fullText, selectionStart)) {
+ return this.members.filter(member => this.assignees.includes(member.username));
+ }
+
+ return this.members;
+ },
+ menuItemTemplate({ original }) {
+ const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
+
+ const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
+ gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
+
+ const avatarTag = original.avatar_url
+ ? `<img
+ src="${original.avatar_url}"
+ alt="${original.username}'s avatar"
+ class="${avatarClasses}"/>`
+ : `<div class="${avatarClasses}">${original.username.charAt(0).toUpperCase()}</div>`;
+
+ const name = escape(original.name);
+
+ const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
+
+ const icon = original.mentionsDisabled
+ ? spriteIcon('notifications-off', 's16 gl-vertical-align-middle gl-ml-3')
+ : '';
+
+ return `${avatarTag}
+ ${original.username}
+ <small class="gl-text-small gl-font-weight-normal gl-reset-color">${name}${count}</small>
+ ${icon}`;
+ },
+ },
+};
+
export default {
name: 'GlMentions',
props: {
@@ -47,67 +110,64 @@ export default {
default: () => gl.GfmAutoComplete?.dataSources || {},
},
},
- data() {
- return {
- assignees: undefined,
- members: undefined,
- };
- },
mounted() {
+ const NON_WORD_OR_INTEGER = /\W|^\d+$/;
+
this.tribute = new Tribute({
- trigger: '@',
- fillAttr: 'username',
- lookup: value => value.name + value.username,
- menuItemTemplate,
- values: this.getMembers,
+ collection: [
+ {
+ trigger: '#',
+ lookup: value => value.iid + value.title,
+ menuItemTemplate: autoCompleteMap[AutoComplete.Issues].menuItemTemplate,
+ selectTemplate: ({ original }) => original.reference || `#${original.iid}`,
+ values: this.getValues(AutoComplete.Issues),
+ },
+ {
+ trigger: '@',
+ fillAttr: 'username',
+ lookup: value => value.name + value.username,
+ menuItemTemplate: autoCompleteMap[AutoComplete.Members].menuItemTemplate,
+ values: this.getValues(AutoComplete.Members),
+ },
+ {
+ trigger: '~',
+ lookup: 'title',
+ menuItemTemplate: autoCompleteMap[AutoComplete.Labels].menuItemTemplate,
+ selectTemplate: ({ original }) =>
+ NON_WORD_OR_INTEGER.test(original.title)
+ ? `~"${original.title}"`
+ : `~${original.title}`,
+ values: this.getValues(AutoComplete.Labels),
+ },
+ ],
});
- const input = this.$slots.default[0].elm;
+ const input = this.$slots.default?.[0]?.elm;
this.tribute.attach(input);
},
beforeDestroy() {
- const input = this.$slots.default[0].elm;
+ const input = this.$slots.default?.[0]?.elm;
this.tribute.detach(input);
},
methods: {
- /**
- * Creates the list of users to show in the mentions dropdown.
- *
- * @param inputText - The text entered by the user in the mentions input field
- * @param processValues - Callback function to set the list of users to show in the mentions dropdown
- */
- getMembers(inputText, processValues) {
- if (this.members) {
- processValues(this.getFilteredMembers());
- } else if (this.dataSources.members) {
- axios
- .get(this.dataSources.members)
- .then(response => {
- this.members = response.data;
- processValues(this.getFilteredMembers());
- })
- .catch(() => {});
- } else {
- processValues([]);
- }
- },
- getFilteredMembers() {
- const fullText = this.$slots.default[0].elm.value;
-
- if (!this.assignees) {
- this.assignees =
- SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
- }
-
- if (fullText.startsWith('/assign @')) {
- return this.members.filter(member => !this.assignees.includes(member.username));
- }
-
- if (fullText.startsWith('/unassign @')) {
- return this.members.filter(member => this.assignees.includes(member.username));
- }
-
- return this.members;
+ getValues(autoCompleteType) {
+ return (inputText, processValues) => {
+ if (this[autoCompleteType]) {
+ const filteredValues = autoCompleteMap[autoCompleteType].filterValues.call(this);
+ processValues(filteredValues);
+ } else if (this.dataSources[autoCompleteType]) {
+ axios
+ .get(this.dataSources[autoCompleteType])
+ .then(response => {
+ this[autoCompleteType] = response.data;
+ const filteredValues = autoCompleteMap[autoCompleteType].filterValues.call(this);
+ processValues(filteredValues);
+ })
+ .catch(() => {});
+ } else {
+ processValues([]);
+ }
+ };
},
},
render(createElement) {
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 2665bb4aa92..2625fcc9d09 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -105,7 +105,7 @@ export default {
</template>
</section>
- <section v-if="$slots.default" class="header-action-buttons">
+ <section v-if="$slots.default" data-testid="headerButtons" class="gl-display-flex">
<slot></slot>
</section>
<gl-deprecated-button
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 80908cbbc9c..68eeadf0f25 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -61,7 +61,12 @@ export default {
</script>
<template>
- <svg :class="[iconSizeClass, iconTestClass]" aria-hidden="true" v-on="$listeners">
+ <svg
+ :key="spriteHref"
+ :class="[iconSizeClass, iconTestClass]"
+ aria-hidden="true"
+ v-on="$listeners"
+ >
<use v-bind="{ 'xlink:href': spriteHref }" />
</svg>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js b/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js
new file mode 100644
index 00000000000..18bfcc268dc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import IssuableHeaderWarnings from './issuable_header_warnings.vue';
+
+export default function issuableHeaderWarnings(store) {
+ return new Vue({
+ el: document.getElementById('js-issuable-header-warnings'),
+ store,
+ render(createElement) {
+ return createElement(IssuableHeaderWarnings);
+ },
+ });
+}
diff --git a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue b/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
new file mode 100644
index 00000000000..37995b434c4
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
@@ -0,0 +1,43 @@
+<script>
+import { mapGetters } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ computed: {
+ ...mapGetters(['getNoteableData']),
+ isLocked() {
+ return this.getNoteableData.discussion_locked;
+ },
+ isConfidential() {
+ return this.getNoteableData.confidential;
+ },
+ warningIconsMeta() {
+ return [
+ {
+ iconName: 'lock',
+ visible: this.isLocked,
+ dataTestId: 'locked',
+ },
+ {
+ iconName: 'eye-slash',
+ visible: this.isConfidential,
+ dataTestId: 'confidential',
+ },
+ ];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-inline-block">
+ <template v-for="meta in warningIconsMeta">
+ <div v-if="meta.visible" :key="meta.iconName" class="issuable-warning-icon inline">
+ <gl-icon :name="meta.iconName" :data-testid="meta.dataTestId" class="icon" />
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
index 1524b313f9f..3006ba83f98 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
@@ -86,12 +86,13 @@ export default {
:img-css-classes="imgCssClasses"
:img-src="avatarUrl(assignee)"
:img-size="iconSize"
- class="js-no-trigger"
+ class="js-no-trigger author-link"
tooltip-placement="bottom"
+ data-qa-selector="assignee_link"
>
<span class="js-assignee-tooltip">
<span class="bold d-block">{{ __('Assignee') }}</span> {{ assignee.name }}
- <span class="text-white-50">@{{ assignee.username }}</span>
+ <span v-if="assignee.username" class="text-white-50">@{{ assignee.username }}</span>
</span>
</user-avatar-link>
<span
@@ -100,6 +101,7 @@ export default {
:title="assigneesCounterTooltip"
class="avatar-counter"
data-placement="bottom"
+ data-qa-selector="avatar_counter_content"
>{{ assigneeCounterLabel }}</span
>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index caf13bc898b..1662e7923b7 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -4,6 +4,7 @@ import { GlIcon, GlTooltip, GlTooltipDirective } from '@gitlab/ui';
import { sprintf } from '~/locale';
import IssueMilestone from './issue_milestone.vue';
import IssueAssignees from './issue_assignees.vue';
+import IssueDueDate from '~/boards/components/issue_due_date.vue';
import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
import CiIcon from '../ci_icon.vue';
@@ -15,6 +16,8 @@ export default {
CiIcon,
GlIcon,
GlTooltip,
+ IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
+ IssueDueDate,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -38,13 +41,6 @@ export default {
},
);
},
- heightStyle() {
- return {
- minHeight: '32px',
- width: '0px',
- visibility: 'hidden',
- };
- },
iconClasses() {
return `${this.iconClass} ic-${this.iconName}`;
},
@@ -60,7 +56,9 @@ export default {
}"
class="item-body d-flex align-items-center py-2 px-3"
>
- <div class="item-contents d-flex align-items-center flex-wrap flex-grow-1 flex-xl-nowrap">
+ <div
+ class="item-contents gl-display-flex gl-align-items-center gl-flex-wrap gl-flex-grow-1 flex-xl-nowrap gl-min-h-7"
+ >
<!-- Title area: Status icon (XL) and title -->
<div class="item-title d-flex align-items-xl-center mb-xl-0">
<div ref="iconElementXL">
@@ -125,8 +123,21 @@ export default {
/>
<!-- Flex order for slots is defined in the parent component: e.g. related_issues_block.vue -->
- <slot name="dueDate"></slot>
- <slot name="weight"></slot>
+ <span v-if="weight > 0" class="order-md-1">
+ <issue-weight
+ :weight="weight"
+ class="item-weight gl-display-flex gl-align-items-center"
+ tag-name="span"
+ />
+ </span>
+
+ <span v-if="dueDate" class="order-md-1">
+ <issue-due-date
+ :date="dueDate"
+ tooltip-placement="top"
+ css-class="item-due-date gl-display-flex gl-align-items-center"
+ />
+ </span>
<issue-assignees
v-if="hasAssignees"
@@ -159,9 +170,5 @@ export default {
>
<icon :size="16" class="btn-item-remove-icon" name="close" />
</button>
-
- <!-- This element serves to set the issue card's height at a minimum of 32 px. -->
- <!-- It fixes #59594: when the remove button is missing, issues have inconsistent heights. -->
- <span :style="heightStyle"></span>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index f954b8eb4f4..6df0119c3db 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -4,7 +4,7 @@ import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash';
import { __, sprintf } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import GLForm from '~/gl_form';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
@@ -167,11 +167,11 @@ export default {
return new GLForm($(this.$refs['gl-form']), {
emojis: this.enableAutocomplete,
members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
- issues: this.enableAutocomplete,
+ issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
mergeRequests: this.enableAutocomplete,
epics: this.enableAutocomplete,
milestones: this.enableAutocomplete,
- labels: this.enableAutocomplete,
+ labels: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
snippets: this.enableAutocomplete,
});
},
@@ -250,7 +250,7 @@ export default {
</gl-mentions>
<slot v-else name="textarea"></slot>
<a
- class="zen-control zen-control-leave js-zen-leave gl-text-gray-700"
+ class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
href="#"
:aria-label="__('Leave zen mode')"
>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 049f5e71849..7e6edcfbd25 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -1,6 +1,8 @@
<script>
import $ from 'jquery';
-import { GlPopover, GlDeprecatedButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { getSelectedFragment } from '~/lib/utils/common_utils';
+import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm';
import ToolbarButton from './toolbar_button.vue';
import Icon from '../icon.vue';
@@ -9,7 +11,7 @@ export default {
ToolbarButton,
Icon,
GlPopover,
- GlDeprecatedButton,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -35,6 +37,11 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ tag: '> ',
+ };
+ },
computed: {
mdTable() {
return [
@@ -81,6 +88,24 @@ export default {
handleSuggestDismissed() {
this.$emit('handleSuggestDismissed');
},
+ handleQuote() {
+ const documentFragment = getSelectedFragment();
+
+ if (!documentFragment || !documentFragment.textContent) {
+ this.tag = '> ';
+ return;
+ }
+ this.tag = '';
+
+ const transformed = CopyAsGFM.transformGFMSelection(documentFragment);
+ const area = this.$el.parentNode.querySelector('textarea');
+
+ CopyAsGFM.nodeToGFM(transformed)
+ .then(gfm => {
+ CopyAsGFM.insertPastedText(area, documentFragment.textContent, CopyAsGFM.quoted(gfm));
+ })
+ .catch(() => {});
+ },
},
};
</script>
@@ -108,9 +133,10 @@ export default {
<toolbar-button tag="*" :button-title="__('Add italic text')" icon="italic" />
<toolbar-button
:prepend="true"
- tag="> "
+ :tag="tag"
:button-title="__('Insert a quote')"
icon="quote"
+ @click="handleQuote"
/>
</div>
<div class="d-inline-block ml-md-2 ml-0">
@@ -141,9 +167,14 @@ export default {
)
}}
</p>
- <gl-deprecated-button variant="primary" size="sm" @click="handleSuggestDismissed">
+ <gl-button
+ variant="info"
+ category="primary"
+ size="sm"
+ @click="handleSuggestDismissed"
+ >
{{ __('Got it') }}
- </gl-deprecated-button>
+ </gl-button>
</gl-popover>
</template>
<toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" />
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 9527c5114f2..1216484b35f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -2,7 +2,7 @@
import Vue from 'vue';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
export default {
props: {
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
index dd1da847001..c08659919fa 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
@@ -1,13 +1,11 @@
import { __ } from '~/locale';
-import { generateToolbarItem } from './services/editor_service';
-import buildCustomHTMLRenderer from './services/build_custom_renderer';
export const CUSTOM_EVENTS = {
openAddImageModal: 'gl_openAddImageModal',
};
/* eslint-disable @gitlab/require-i18n-strings */
-const TOOLBAR_ITEM_CONFIGS = [
+export const TOOLBAR_ITEM_CONFIGS = [
{ icon: 'heading', event: 'openHeadingSelect', classes: 'tui-heading', tooltip: __('Headings') },
{ icon: 'bold', command: 'Bold', tooltip: __('Add bold text') },
{ icon: 'italic', command: 'Italic', tooltip: __('Add italic text') },
@@ -30,11 +28,6 @@ const TOOLBAR_ITEM_CONFIGS = [
{ icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
];
-export const EDITOR_OPTIONS = {
- toolbarItems: TOOLBAR_ITEM_CONFIGS.map(config => generateToolbarItem(config)),
- customHTMLRenderer: buildCustomHTMLRenderer(),
-};
-
export const EDITOR_TYPES = {
markdown: 'markdown',
wysiwyg: 'wysiwyg',
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue
index 0a444b2295d..429a4e04110 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue
@@ -1,6 +1,6 @@
<script>
-import { isSafeURL } from '~/lib/utils/url_utility';
import { GlModal, GlFormGroup, GlFormInput, GlTabs, GlTab } from '@gitlab/ui';
+import { isSafeURL } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IMAGE_TABS } from '../../constants';
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue
index 739f8b502c9..9baa7f286d7 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab.vue
@@ -1,6 +1,6 @@
<script>
-import { __ } from '~/locale';
import { GlFormGroup } from '@gitlab/ui';
+import { __ } from '~/locale';
import { MAX_FILE_SIZE } from '../../constants';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
index baeb98bec75..d96fe46522e 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
@@ -3,16 +3,11 @@ import 'codemirror/lib/codemirror.css';
import '@toast-ui/editor/dist/toastui-editor.css';
import AddImageModal from './modals/add_image/add_image_modal.vue';
-import {
- EDITOR_OPTIONS,
- EDITOR_TYPES,
- EDITOR_HEIGHT,
- EDITOR_PREVIEW_STYLE,
- CUSTOM_EVENTS,
-} from './constants';
+import { EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE, CUSTOM_EVENTS } from './constants';
import {
registerHTMLToMarkdownRenderer,
+ getEditorOptions,
addCustomEventListener,
removeCustomEventListener,
addImage,
@@ -35,7 +30,7 @@ export default {
options: {
type: Object,
required: false,
- default: () => EDITOR_OPTIONS,
+ default: () => null,
},
initialEditType: {
type: String,
@@ -65,13 +60,13 @@ export default {
};
},
computed: {
- editorOptions() {
- return { ...EDITOR_OPTIONS, ...this.options };
- },
editorInstance() {
return this.$refs.editor;
},
},
+ created() {
+ this.editorOptions = getEditorOptions(this.options);
+ },
beforeDestroy() {
this.removeListeners();
},
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
index 70d29b5b3df..a9c5d442f62 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_custom_renderer.js
@@ -1,16 +1,18 @@
+import { union, mapValues } from 'lodash';
import renderBlockHtml from './renderers/render_html_block';
import renderKramdownList from './renderers/render_kramdown_list';
import renderKramdownText from './renderers/render_kramdown_text';
import renderIdentifierInstanceText from './renderers/render_identifier_instance_text';
import renderIdentifierParagraph from './renderers/render_identifier_paragraph';
-import renderEmbeddedRubyText from './renderers/render_embedded_ruby_text';
import renderFontAwesomeHtmlInline from './renderers/render_font_awesome_html_inline';
+import renderSoftbreak from './renderers/render_softbreak';
const htmlInlineRenderers = [renderFontAwesomeHtmlInline];
const htmlBlockRenderers = [renderBlockHtml];
const listRenderers = [renderKramdownList];
const paragraphRenderers = [renderIdentifierParagraph];
-const textRenderers = [renderKramdownText, renderEmbeddedRubyText, renderIdentifierInstanceText];
+const textRenderers = [renderKramdownText, renderIdentifierInstanceText];
+const softbreakRenderers = [renderSoftbreak];
const executeRenderer = (renderers, node, context) => {
const availableRenderer = renderers.find(renderer => renderer.canRender(node, context));
@@ -18,51 +20,20 @@ const executeRenderer = (renderers, node, context) => {
return availableRenderer ? availableRenderer.render(node, context) : context.origin();
};
-const buildCustomRendererFunctions = (customRenderers, defaults) => {
- const customTypes = Object.keys(customRenderers).filter(type => !defaults[type]);
- const customEntries = customTypes.map(type => {
- const fn = (node, context) => executeRenderer(customRenderers[type], node, context);
- return [type, fn];
- });
-
- return Object.fromEntries(customEntries);
-};
-
-const buildCustomHTMLRenderer = (
- customRenderers = { htmlBlock: [], htmlInline: [], list: [], paragraph: [], text: [] },
-) => {
- const defaults = {
- htmlBlock(node, context) {
- const allHtmlBlockRenderers = [...customRenderers.htmlBlock, ...htmlBlockRenderers];
-
- return executeRenderer(allHtmlBlockRenderers, node, context);
- },
- htmlInline(node, context) {
- const allHtmlInlineRenderers = [...customRenderers.htmlInline, ...htmlInlineRenderers];
-
- return executeRenderer(allHtmlInlineRenderers, node, context);
- },
- list(node, context) {
- const allListRenderers = [...customRenderers.list, ...listRenderers];
-
- return executeRenderer(allListRenderers, node, context);
- },
- paragraph(node, context) {
- const allParagraphRenderers = [...customRenderers.paragraph, ...paragraphRenderers];
-
- return executeRenderer(allParagraphRenderers, node, context);
- },
- text(node, context) {
- const allTextRenderers = [...customRenderers.text, ...textRenderers];
-
- return executeRenderer(allTextRenderers, node, context);
- },
+const buildCustomHTMLRenderer = customRenderers => {
+ const renderersByType = {
+ ...customRenderers,
+ htmlBlock: union(htmlBlockRenderers, customRenderers?.htmlBlock),
+ htmlInline: union(htmlInlineRenderers, customRenderers?.htmlInline),
+ list: union(listRenderers, customRenderers?.list),
+ paragraph: union(paragraphRenderers, customRenderers?.paragraph),
+ text: union(textRenderers, customRenderers?.text),
+ softbreak: union(softbreakRenderers, customRenderers?.softbreak),
};
- return {
- ...buildCustomRendererFunctions(customRenderers, defaults),
- ...defaults,
- };
+ return mapValues(renderersByType, renderers => {
+ return (node, context) => executeRenderer(renderers, node, context);
+ });
};
export default buildCustomHTMLRenderer;
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
index ed04765c871..868ede9426e 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer.js
@@ -1,7 +1,12 @@
+/* eslint-disable @gitlab/require-i18n-strings */
import { defaults, repeat } from 'lodash';
const DEFAULTS = {
subListIndentSpaces: 4,
+ unorderedListBulletChar: '-',
+ incrementListMarker: false,
+ strong: '*',
+ emphasis: '_',
};
const countIndentSpaces = text => {
@@ -11,9 +16,18 @@ const countIndentSpaces = text => {
};
const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) => {
- const { subListIndentSpaces } = defaults(formattingPreferences, DEFAULTS);
- // eslint-disable-next-line @gitlab/require-i18n-strings
+ const {
+ subListIndentSpaces,
+ unorderedListBulletChar,
+ incrementListMarker,
+ strong,
+ emphasis,
+ } = defaults(formattingPreferences, DEFAULTS);
const sublistNode = 'LI OL, LI UL';
+ const unorderedListItemNode = 'UL LI';
+ const orderedListItemNode = 'OL LI';
+ const emphasisNode = 'EM, I';
+ const strongNode = 'STRONG, B';
return {
TEXT_NODE(node) {
@@ -47,6 +61,27 @@ const buildHTMLToMarkdownRender = (baseRenderer, formattingPreferences = {}) =>
return reindentedList;
},
+ [unorderedListItemNode](node, subContent) {
+ const baseResult = baseRenderer.convert(node, subContent);
+
+ return baseResult.replace(/^(\s*)([*|-])/, `$1${unorderedListBulletChar}`);
+ },
+ [orderedListItemNode](node, subContent) {
+ const baseResult = baseRenderer.convert(node, subContent);
+
+ return incrementListMarker ? baseResult : baseResult.replace(/^(\s*)\d+?\./, '$11.');
+ },
+ [emphasisNode](node, subContent) {
+ const result = baseRenderer.convert(node, subContent);
+
+ return result.replace(/(^[*_]{1}|[*_]{1}$)/g, emphasis);
+ },
+ [strongNode](node, subContent) {
+ const result = baseRenderer.convert(node, subContent);
+ const strongSyntax = repeat(strong, 2);
+
+ return result.replace(/^[*_]{2}/, strongSyntax).replace(/[*_]{2}$/, strongSyntax);
+ },
};
};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
index 6436dcaae64..51ba033dff0 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/editor_service.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
+import { defaults } from 'lodash';
import ToolbarItem from '../toolbar_item.vue';
import buildHtmlToMarkdownRenderer from './build_html_to_markdown_renderer';
+import buildCustomHTMLRenderer from './build_custom_renderer';
+import { TOOLBAR_ITEM_CONFIGS } from '../constants';
const buildWrapper = propsData => {
const instance = new Vue({
@@ -54,3 +57,10 @@ export const registerHTMLToMarkdownRenderer = editorApi => {
renderer: renderer.constructor.factory(renderer, buildHtmlToMarkdownRenderer(renderer)),
});
};
+
+export const getEditorOptions = externalOptions => {
+ return defaults({
+ customHTMLRenderer: buildCustomHTMLRenderer(externalOptions?.customRenderers),
+ toolbarItems: TOOLBAR_ITEM_CONFIGS.map(toolbarItem => generateToolbarItem(toolbarItem)),
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js
index d96cadafdbb..1dcecd5fb8c 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token.js
@@ -34,7 +34,7 @@ export const buildUneditableCloseTokens = (token, tagType = TAG_TYPES.block) =>
export const buildTextToken = content => buildToken('text', null, { content });
-export const buildUneditableTokens = token => {
+export const buildUneditableBlockTokens = token => {
return [...buildUneditableOpenTokens(token), buildUneditableCloseToken()];
};
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js
index 494057fc75b..0e122f598e5 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text.js
@@ -1,4 +1,4 @@
-import { buildUneditableTokens } from './build_uneditable_token';
+import { renderUneditableLeaf as render } from './render_utils';
const embeddedRubyRegex = /(^<%.+%>$)/;
@@ -6,8 +6,4 @@ const canRender = ({ literal }) => {
return embeddedRubyRegex.test(literal);
};
-const render = (_, { origin }) => {
- return buildUneditableTokens(origin());
-};
-
export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
index f5b4502ea3c..4ec45ecd3a7 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph.js
@@ -1,4 +1,4 @@
-import { buildUneditableOpenTokens, buildUneditableCloseToken } from './build_uneditable_token';
+import { renderUneditableBranch as render } from './render_utils';
const identifierRegex = /(^\[.+\]: .+)/;
@@ -10,7 +10,4 @@ const canRender = (node, context) => {
return isIdentifier(context.getChildrenText(node));
};
-const render = (_, { entering, origin }) =>
- entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
-
export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js
index 491a26c81d0..949ca0e5c2a 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list.js
@@ -1,4 +1,4 @@
-import { buildUneditableOpenTokens, buildUneditableCloseToken } from './build_uneditable_token';
+import { renderUneditableBranch as render } from './render_utils';
const isKramdownTOC = ({ type, literal }) => type === 'text' && literal === 'TOC';
@@ -21,7 +21,4 @@ const canRender = node => {
return false;
};
-const render = (_, { entering, origin }) =>
- entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
-
export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js
index 01384699e4f..0551894918c 100644
--- a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text.js
@@ -1,4 +1,4 @@
-import { buildUneditableTokens } from './build_uneditable_token';
+import { renderUneditableLeaf as render } from './render_utils';
const kramdownRegex = /(^{:.+}$)/;
@@ -6,8 +6,4 @@ const canRender = ({ literal }) => {
return kramdownRegex.test(literal);
};
-const render = (_, { origin }) => {
- return buildUneditableTokens(origin());
-};
-
export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js
new file mode 100644
index 00000000000..389ade5f27a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_softbreak.js
@@ -0,0 +1,7 @@
+const canRender = node => ['emph', 'strong'].includes(node.parent?.type);
+const render = () => ({
+ type: 'text',
+ content: ' ',
+});
+
+export default { canRender, render };
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js
new file mode 100644
index 00000000000..cec6491557b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/services/renderers/render_utils.js
@@ -0,0 +1,10 @@
+import {
+ buildUneditableBlockTokens,
+ buildUneditableOpenTokens,
+ buildUneditableCloseToken,
+} from './build_uneditable_token';
+
+export const renderUneditableLeaf = (_, { origin }) => buildUneditableBlockTokens(origin());
+
+export const renderUneditableBranch = (_, { entering, origin }) =>
+ entering ? buildUneditableOpenTokens(origin()) : buildUneditableCloseToken();
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
index 1be5284fa9c..9b28ce0d881 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
@@ -1,6 +1,6 @@
<script>
-import { __, s__, sprintf } from '~/locale';
import { GlIcon } from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
export default {
components: {
@@ -78,7 +78,7 @@ export default {
<span class="dropdown-toggle-text"> {{ dropdownToggleText }} </span>
<gl-icon
name="chevron-down"
- class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-700"
+ class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
:size="16"
/>
</button>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
index f0a846c4924..6222dfc5853 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
@@ -18,11 +18,11 @@ export default {
/>
<gl-icon
name="search"
- class="dropdown-input-search gl-absolute gl-top-3 gl-right-5 gl-text-gray-500 gl-pointer-events-none"
+ class="dropdown-input-search gl-absolute gl-top-3 gl-right-5 gl-text-gray-300 gl-pointer-events-none"
/>
<gl-icon
name="close"
- class="dropdown-input-clear js-dropdown-input-clear gl-absolute gl-top-3 gl-right-5 gl-text-gray-700"
+ class="dropdown-input-clear js-dropdown-input-clear gl-absolute gl-top-3 gl-right-5 gl-text-gray-500"
/>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
index 69fb2bb4524..91cf5d6bef5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
@@ -15,7 +15,7 @@ export default {
</script>
<template>
- <div class="title hide-collapsed append-bottom-10">
+ <div class="title hide-collapsed gl-mb-3">
{{ __('Labels') }}
<template v-if="canEdit">
<gl-loading-icon inline class="align-text-top block-loading" />
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
index cf77aa37d14..c65266fce5a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
@@ -19,6 +19,9 @@ export default {
handleButtonClick(e) {
if (this.isDropdownVariantStandalone || this.isDropdownVariantEmbedded) {
this.toggleDropdownContents();
+ }
+
+ if (this.isDropdownVariantStandalone) {
e.stopPropagation();
}
},
@@ -31,9 +34,9 @@ export default {
class="labels-select-dropdown-button js-dropdown-button w-100 text-left"
@click="handleButtonClick"
>
- <span class="dropdown-toggle-text flex-fill">
+ <span class="dropdown-toggle-text gl-pointer-events-none flex-fill">
{{ dropdownButtonText }}
</span>
- <gl-icon name="chevron-down" class="pull-right" />
+ <gl-icon name="chevron-down" class="gl-pointer-events-none float-right" />
</gl-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
index ef8218b5135..6839354fb3a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
@@ -9,6 +9,13 @@ export default {
DropdownContentsLabelsView,
DropdownContentsCreateView,
},
+ props: {
+ renderOnTop: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
computed: {
...mapState(['showDropdownContentsCreateView']),
dropdownContentsView() {
@@ -17,6 +24,13 @@ export default {
}
return 'dropdown-contents-labels-view';
},
+ directionStyle() {
+ if (this.renderOnTop) {
+ return { bottom: '100%' };
+ }
+
+ return {};
+ },
},
};
</script>
@@ -24,6 +38,7 @@ export default {
<template>
<div
class="labels-select-dropdown-contents w-100 mt-1 mb-3 py-2 rounded-top rounded-bottom position-absolute"
+ :style="directionStyle"
>
<component :is="dropdownContentsView" />
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
index 94671f8a109..55e2fb68275 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
@@ -105,13 +105,13 @@ export default {
:disabled="disableCreate"
category="primary"
variant="success"
- class="pull-left d-flex align-items-center"
+ class="float-left d-flex align-items-center"
@click="handleCreateClick"
>
<gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" />
{{ __('Create') }}
</gl-button>
- <gl-button class="pull-right js-btn-cancel-create" @click="toggleDropdownContentsCreateView">
+ <gl-button class="float-right js-btn-cancel-create" @click="toggleDropdownContentsCreateView">
{{ __('Cancel') }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index ef506d00d9a..0b763aa4b72 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -45,6 +45,16 @@ export default {
}
return this.labels;
},
+ showListContainer() {
+ if (this.isDropdownVariantSidebar) {
+ return !this.labelsFetchInProgress;
+ }
+
+ return true;
+ },
+ showNoMatchingResultsMessage() {
+ return !this.labelsFetchInProgress && !this.visibleLabels.length;
+ },
},
watch: {
searchKey(value) {
@@ -132,6 +142,7 @@ export default {
<div
v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
+ data-testid="dropdown-title"
>
<span class="flex-grow-1">{{ labelsListTitle }}</span>
<gl-button
@@ -146,7 +157,12 @@ export default {
<div class="dropdown-input" @click.stop="() => {}">
<gl-search-box-by-type v-model="searchKey" :autofocus="true" />
</div>
- <div v-show="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
+ <div
+ v-show="showListContainer"
+ ref="labelsListContainer"
+ class="dropdown-content"
+ data-testid="dropdown-content"
+ >
<smart-virtual-list
:length="visibleLabels.length"
:remain="$options.LIST_BUFFER_SIZE"
@@ -163,12 +179,16 @@ export default {
@clickLabel="handleLabelClick(label)"
/>
</li>
- <li v-show="!visibleLabels.length" class="p-2 text-center">
+ <li v-show="showNoMatchingResultsMessage" class="gl-p-3 gl-text-center">
{{ __('No matching results') }}
</li>
</smart-virtual-list>
</div>
- <div v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded" class="dropdown-footer">
+ <div
+ v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
+ class="dropdown-footer"
+ data-testid="dropdown-footer"
+ >
<ul class="list-unstyled">
<li v-if="allowLabelCreate">
<gl-link
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
index 081c892e09f..2d6a4a9758c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
@@ -1,10 +1,10 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlLoadingIcon,
},
props: {
@@ -23,16 +23,16 @@ export default {
</script>
<template>
- <div class="title hide-collapsed append-bottom-10">
+ <div class="title hide-collapsed gl-mb-3">
{{ __('Labels') }}
<template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" inline />
- <gl-deprecated-button
+ <gl-button
variant="link"
- class="pull-right js-sidebar-dropdown-toggle"
+ class="float-right js-sidebar-dropdown-toggle"
data-qa-selector="labels_edit_button"
@click="toggleDropdownContents"
- >{{ __('Edit') }}</gl-deprecated-button
+ >{{ __('Edit') }}</gl-button
>
</template>
</div>
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 258a87e62b9..248e9929833 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
@@ -2,6 +2,7 @@
import $ from 'jquery';
import Vue from 'vue';
import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
+import { isInViewport } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
@@ -100,6 +101,11 @@ export default {
default: __('Manage group labels'),
},
},
+ data() {
+ return {
+ contentIsOnViewport: true,
+ };
+ },
computed: {
...mapState(['showDropdownButton', 'showDropdownContents']),
...mapGetters([
@@ -117,6 +123,9 @@ export default {
selectedLabels,
});
},
+ showDropdownContents(showDropdownContents) {
+ this.setContentIsOnViewport(showDropdownContents);
+ },
},
mounted() {
this.setInitialState({
@@ -203,6 +212,20 @@ export default {
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
+ setContentIsOnViewport(showDropdownContents) {
+ if (!this.isDropdownVariantEmbedded || !showDropdownContents) {
+ this.contentIsOnViewport = true;
+
+ return;
+ }
+
+ this.$nextTick(() => {
+ if (this.$refs.dropdownContents) {
+ const offset = { top: 100 };
+ this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el, offset);
+ }
+ });
+ },
},
};
</script>
@@ -239,6 +262,7 @@ export default {
<dropdown-contents
v-if="dropdownButtonVisible && showDropdownContents"
ref="dropdownContents"
+ :render-on-top="!contentIsOnViewport"
/>
</template>
</div>
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 e6053628eca..e624bd1eaee 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 flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
@@ -56,6 +56,3 @@ export const createLabel = ({ state, dispatch }, label) => {
export const updateSelectedLabels = ({ commit }, labels) =>
commit(types.UPDATE_SELECTED_LABELS, { labels });
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
index e035a866048..5a30e29cad3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
@@ -50,6 +50,3 @@ export const isDropdownVariantStandalone = state => state.variant === DropdownVa
* @param {object} state
*/
export const isDropdownVariantEmbedded = state => state.variant === DropdownVariant.Embedded;
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue
index b11ec8b8838..e9b99c6ea78 100644
--- a/app/assets/javascripts/vue_shared/components/split_button.vue
+++ b/app/assets/javascripts/vue_shared/components/split_button.vue
@@ -1,15 +1,19 @@
<script>
import { isString } from 'lodash';
-import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui';
+import {
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownDivider,
+ GlDeprecatedDropdownItem,
+} from '@gitlab/ui';
const isValidItem = item =>
isString(item.eventName) && isString(item.title) && isString(item.description);
export default {
components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownDivider,
+ GlDeprecatedDropdownItem,
},
props: {
@@ -57,7 +61,7 @@ export default {
</script>
<template>
- <gl-dropdown
+ <gl-deprecated-dropdown
:menu-class="`dropdown-menu-selectable ${menuClass}`"
split
:text="dropdownToggleText"
@@ -66,7 +70,7 @@ export default {
@click="triggerEvent"
>
<template v-for="(item, itemIndex) in actionItems">
- <gl-dropdown-item
+ <gl-deprecated-dropdown-item
:key="item.eventName"
:active="selectedItem === item"
active-class="is-active"
@@ -74,12 +78,12 @@ export default {
>
<strong>{{ item.title }}</strong>
<div>{{ item.description }}</div>
- </gl-dropdown-item>
+ </gl-deprecated-dropdown-item>
- <gl-dropdown-divider
+ <gl-deprecated-dropdown-divider
v-if="itemIndex < actionItems.length - 1"
:key="`${item.eventName}-divider`"
/>
</template>
- </gl-dropdown>
+ </gl-deprecated-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
index b1a4f3dccaf..4447a87777a 100644
--- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
+++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue
@@ -1,5 +1,6 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
+
import timeagoMixin from '../mixins/timeago';
import '../../lib/utils/datetime_utility';
@@ -28,6 +29,11 @@ export default {
default: '',
},
},
+ computed: {
+ timeAgo() {
+ return this.timeFormatted(this.time);
+ },
+ },
};
</script>
<template>
@@ -35,7 +41,7 @@ export default {
v-gl-tooltip.viewport="{ placement: tooltipPlacement }"
:class="cssClass"
:title="tooltipTitle(time)"
- v-text="timeFormatted(time)"
+ :datetime="time"
+ ><slot :timeAgo="timeAgo">{{ timeAgo }}</slot></time
>
- </time>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
new file mode 100644
index 00000000000..148bd501a8e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
@@ -0,0 +1,102 @@
+<script>
+import { GlNewDropdown, GlDeprecatedDropdownItem, GlSearchBoxByType, GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+
+export default {
+ name: 'TimezoneDropdown',
+ components: {
+ GlNewDropdown,
+ GlDeprecatedDropdownItem,
+ GlSearchBoxByType,
+ GlIcon,
+ },
+ 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;
+ },
+ 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)}`;
+ },
+ formatTimezone(item) {
+ return `[UTC ${this.formatUtcOffset(item.offset)}] ${item.name}`;
+ },
+ },
+};
+</script>
+<template>
+ <gl-new-dropdown :text="value" block lazy menu-class="gl-w-full!">
+ <template #button-content>
+ <span class="gl-flex-grow-1" :class="{ 'gl-text-gray-300': !value }">
+ {{ selectedTimezoneLabel }}
+ </span>
+ <gl-icon name="chevron-down" />
+ </template>
+
+ <gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus class="gl-m-3" />
+ <gl-deprecated-dropdown-item
+ v-for="timezone in filteredResults"
+ :key="timezone.formattedTimezone"
+ @click="selectTimezone(timezone)"
+ >
+ <gl-icon
+ :class="{ invisible: !isSelected(timezone) }"
+ name="mobile-issue-close"
+ class="gl-vertical-align-middle"
+ />
+ {{ timezone.formattedTimezone }}
+ </gl-deprecated-dropdown-item>
+ <gl-deprecated-dropdown-item v-if="!filteredResults.length" data-testid="noMatchingResults">
+ {{ $options.tranlations.noResultsText }}
+ </gl-deprecated-dropdown-item>
+ </gl-new-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
index 1de866bed37..540edc9f61c 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_button.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -1,7 +1,6 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { s__ } from '../../locale';
-import icon from './icon.vue';
const ICON_ON = 'status_success_borderless';
const ICON_OFF = 'status_failed_borderless';
@@ -10,7 +9,7 @@ const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF');
export default {
components: {
- icon,
+ GlIcon,
GlLoadingIcon,
},
@@ -63,18 +62,27 @@ export default {
<label class="toggle-wrapper">
<input v-if="name" :name="name" :value="value" type="hidden" />
<button
+ type="button"
+ role="switch"
+ class="project-feature-toggle"
:aria-label="ariaLabel"
+ :aria-checked="value"
:class="{
'is-checked': value,
+ 'gl-blue-500': value,
'is-disabled': disabledInput,
'is-loading': isLoading,
}"
- type="button"
- class="project-feature-toggle"
@click="toggleFeature"
>
<gl-loading-icon class="loading-icon" />
- <span class="toggle-icon"> <icon :name="toggleIcon" class="toggle-icon-svg" /> </span>
+ <span class="toggle-icon">
+ <gl-icon
+ :size="18"
+ :name="toggleIcon"
+ :class="value ? 'gl-text-blue-500' : 'gl-text-gray-400'"
+ />
+ </span>
</button>
</label>
</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 db378d6f977..e19d659c179 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,12 +1,12 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
import UserAvatarLink from './user_avatar_link.vue';
export default {
components: {
UserAvatarLink,
- GlDeprecatedButton,
+ GlButton,
},
props: {
items: {
@@ -82,12 +82,12 @@ export default {
:img-size="imgSize"
/>
<template v-if="hasBreakpoint">
- <gl-deprecated-button v-if="hasHiddenItems" variant="link" @click="expand">
+ <gl-button v-if="hasHiddenItems" variant="link" @click="expand">
{{ expandText }}
- </gl-deprecated-button>
- <gl-deprecated-button v-else variant="link" @click="collapse">
+ </gl-button>
+ <gl-button v-else variant="link" @click="collapse">
{{ __('show less') }}
- </gl-deprecated-button>
+ </gl-button>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index bd35d3fead9..699e466e848 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
@@ -70,20 +70,20 @@ export default {
<h5 class="gl-m-0">
{{ user.name }}
</h5>
- <span class="gl-text-gray-700">@{{ user.username }}</span>
+ <span class="gl-text-gray-500">@{{ user.username }}</span>
</div>
- <div class="gl-text-gray-700">
+ <div class="gl-text-gray-500">
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
- <icon name="profile" class="gl-text-gray-600 gl-flex-shrink-0" />
- <span ref="bio" class="ml-1" v-html="user.bioHtml"></span>
+ <icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
+ <span ref="bio" class="gl-ml-2" v-html="user.bioHtml"></span>
</div>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
- <icon name="work" class="gl-text-gray-600 gl-flex-shrink-0" />
+ <icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
<span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
</div>
</div>
- <div v-if="user.location" class="js-location gl-text-gray-700 gl-display-flex">
- <icon name="location" class="gl-text-gray-600 flex-shrink-0" />
+ <div v-if="user.location" class="js-location gl-text-gray-500 gl-display-flex">
+ <icon name="location" class="gl-text-gray-400 flex-shrink-0" />
<span class="gl-ml-2">{{ user.location }}</span>
</div>
<div v-if="statusHtml" class="js-user-status gl-mt-3">
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 235beb1f22d..5511145fba2 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -41,13 +41,13 @@ export const timeRanges = [
interval: INTERVALS.hour,
},
{
- label: __('1 week'),
+ label: __('7 days'),
duration: { seconds: 60 * 60 * 24 * 7 * 1 },
name: 'oneWeek',
interval: INTERVALS.day,
},
{
- label: __('1 month'),
+ label: __('30 days'),
duration: { seconds: 60 * 60 * 24 * 30 },
name: 'oneMonth',
interval: INTERVALS.day,
diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js
index 817a90f8149..edc31cfa69e 100644
--- a/app/assets/javascripts/vuex_shared/bindings.js
+++ b/app/assets/javascripts/vuex_shared/bindings.js
@@ -9,6 +9,7 @@
* @param {string} root - the key of the state where to search fo they keys described in list
* @returns {Object} a dictionary with all the computed properties generated
*/
+// eslint-disable-next-line import/prefer-default-export
export const mapComputed = (list, defaultUpdateFn, root) => {
const result = {};
list.forEach(item => {
@@ -32,5 +33,3 @@ export const mapComputed = (list, defaultUpdateFn, root) => {
});
return result;
};
-
-export default () => {};
diff --git a/app/assets/javascripts/vuex_shared/modules/modal/actions.js b/app/assets/javascripts/vuex_shared/modules/modal/actions.js
index 7b209909f69..552237e05c5 100644
--- a/app/assets/javascripts/vuex_shared/modules/modal/actions.js
+++ b/app/assets/javascripts/vuex_shared/modules/modal/actions.js
@@ -15,6 +15,3 @@ export const show = ({ commit }) => {
export const hide = ({ commit }) => {
commit(types.HIDE);
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
new file mode 100644
index 00000000000..d974556cb9e
--- /dev/null
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -0,0 +1,29 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { GlDrawer } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDrawer,
+ },
+ computed: {
+ ...mapState(['open']),
+ },
+ methods: {
+ ...mapActions(['closeDrawer']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-drawer class="mt-6" :open="open" @close="closeDrawer">
+ <template #header>
+ <h4>{{ __("What's new at GitLab") }}</h4>
+ </template>
+ <template>
+ <div></div>
+ </template>
+ </gl-drawer>
+ </div>
+</template>
diff --git a/app/assets/javascripts/whats_new/components/trigger.vue b/app/assets/javascripts/whats_new/components/trigger.vue
new file mode 100644
index 00000000000..e6c48e92888
--- /dev/null
+++ b/app/assets/javascripts/whats_new/components/trigger.vue
@@ -0,0 +1,19 @@
+<script>
+import { mapActions } from 'vuex';
+import { GlButton } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ },
+ methods: {
+ ...mapActions(['openDrawer']),
+ },
+};
+</script>
+
+<template>
+ <li>
+ <gl-button variant="link" @click="openDrawer">{{ __("See what's new at GitLab") }}</gl-button>
+ </li>
+</template>
diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js
new file mode 100644
index 00000000000..c9ee3404d2a
--- /dev/null
+++ b/app/assets/javascripts/whats_new/index.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import App from './components/app.vue';
+import Trigger from './components/trigger.vue';
+import store from './store';
+
+export default () => {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: document.getElementById('whats-new-app'),
+ store,
+ components: {
+ App,
+ },
+
+ render(createElement) {
+ return createElement('app');
+ },
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: document.getElementById('whats-new-trigger'),
+ store,
+ components: {
+ Trigger,
+ },
+
+ render(createElement) {
+ return createElement('trigger');
+ },
+ });
+};
diff --git a/app/assets/javascripts/whats_new/store/actions.js b/app/assets/javascripts/whats_new/store/actions.js
new file mode 100644
index 00000000000..53488413d9e
--- /dev/null
+++ b/app/assets/javascripts/whats_new/store/actions.js
@@ -0,0 +1,10 @@
+import * as types from './mutation_types';
+
+export default {
+ closeDrawer({ commit }) {
+ commit(types.CLOSE_DRAWER);
+ },
+ openDrawer({ commit }) {
+ commit(types.OPEN_DRAWER);
+ },
+};
diff --git a/app/assets/javascripts/whats_new/store/index.js b/app/assets/javascripts/whats_new/store/index.js
new file mode 100644
index 00000000000..aea980060aa
--- /dev/null
+++ b/app/assets/javascripts/whats_new/store/index.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import actions from './actions';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ actions,
+ mutations,
+ state,
+});
diff --git a/app/assets/javascripts/whats_new/store/mutation_types.js b/app/assets/javascripts/whats_new/store/mutation_types.js
new file mode 100644
index 00000000000..daa65230101
--- /dev/null
+++ b/app/assets/javascripts/whats_new/store/mutation_types.js
@@ -0,0 +1,2 @@
+export const CLOSE_DRAWER = 'CLOSE_DRAWER';
+export const OPEN_DRAWER = 'OPEN_DRAWER';
diff --git a/app/assets/javascripts/whats_new/store/mutations.js b/app/assets/javascripts/whats_new/store/mutations.js
new file mode 100644
index 00000000000..f7e84ee81a9
--- /dev/null
+++ b/app/assets/javascripts/whats_new/store/mutations.js
@@ -0,0 +1,10 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.CLOSE_DRAWER](state) {
+ state.open = false;
+ },
+ [types.OPEN_DRAWER](state) {
+ state.open = true;
+ },
+};
diff --git a/app/assets/javascripts/whats_new/store/state.js b/app/assets/javascripts/whats_new/store/state.js
new file mode 100644
index 00000000000..97089a095f1
--- /dev/null
+++ b/app/assets/javascripts/whats_new/store/state.js
@@ -0,0 +1,3 @@
+export default {
+ open: false,
+};
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 41fb62c28e6..f5393ef47d6 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -18,8 +18,8 @@
// GitLab UI framework
@import 'framework';
-// Font icons
-@import 'font-awesome';
+// Custom Fontawesome icons
+@import 'fontawesome_custom';
// Page specific styles (issues, projects etc):
@import 'pages/**/*';
@@ -51,3 +51,7 @@
@media print {
@import 'print';
}
+
+/* Rules for overriding cloaking in startup-general.scss */
+@import 'startup/cloaking';
+@include cloak-startup-scss(block);
diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss
index 6bb7e9d215e..67213eedca8 100644
--- a/app/assets/stylesheets/components/avatar.scss
+++ b/app/assets/stylesheets/components/avatar.scss
@@ -125,7 +125,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
.identicon {
text-align: center;
vertical-align: top;
- color: $gray-800;
+ color: $gray-700;
background-color: $gray-darker;
// Sizes
diff --git a/app/assets/stylesheets/components/dashboard_skeleton.scss b/app/assets/stylesheets/components/dashboard_skeleton.scss
index 64091201221..09ba89c0782 100644
--- a/app/assets/stylesheets/components/dashboard_skeleton.scss
+++ b/app/assets/stylesheets/components/dashboard_skeleton.scss
@@ -25,7 +25,7 @@
}
&-icon {
- color: $gray-500;
+ color: $gray-300;
}
&-footer {
@@ -33,7 +33,7 @@
height: $gl-padding-32;
&-arrow {
- color: $gray-300;
+ color: $gray-200;
}
&-downstream {
@@ -41,7 +41,7 @@
}
&-extra {
- background-color: $gray-400;
+ background-color: $gray-200;
font-size: 10px;
line-height: $gl-line-height;
width: $gl-padding;
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
index 33f03fb5949..80421598966 100644
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ b/app/assets/stylesheets/components/design_management/design.scss
@@ -31,7 +31,7 @@
border-radius: 50%;
&.resolved {
- background-color: $gray-700;
+ background-color: $gray-500;
}
}
}
@@ -164,7 +164,7 @@
}
&:hover {
- border-color: $gray-500;
+ border-color: $gray-300;
}
}
diff --git a/app/assets/stylesheets/components/design_management/design_list_item.scss b/app/assets/stylesheets/components/design_management/design_list_item.scss
index 3a6781b666e..b7f6b2026fe 100644
--- a/app/assets/stylesheets/components/design_management/design_list_item.scss
+++ b/app/assets/stylesheets/components/design_management/design_list_item.scss
@@ -1,12 +1,3 @@
-.designs-root {
- border: 2px dashed transparent;
- transition: border $gl-transition-duration-medium $general-hover-transition-curve;
-
- &:hover {
- border-color: $gray-100;
- }
-}
-
.design-list-item {
height: 280px;
text-decoration: none;
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index dd749b4df1a..c0699844387 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -81,30 +81,9 @@ $item-remove-button-space: 42px;
max-width: 0;
}
- .status {
- &-at-risk {
- color: $red-500;
- background-color: $red-100;
- }
-
- &-needs-attention {
- color: $orange-700;
- background-color: $orange-100;
- }
-
- &-on-track {
- color: $green-600;
- background-color: $green-100;
- }
- }
-
- .gl-label-text {
- font-weight: $gl-font-weight-bold;
- }
-
.bullet-separator {
font-size: 9px;
- color: $gray-400;
+ color: $gray-200;
}
}
@@ -213,6 +192,7 @@ $item-remove-button-space: 42px;
margin-right: $gl-padding-4 / 2;
line-height: 0;
border-color: transparent;
+ background-color: transparent;
color: $gl-text-color-secondary;
.related-items-tree & {
diff --git a/app/assets/stylesheets/components/rich_content_editor.scss b/app/assets/stylesheets/components/rich_content_editor.scss
index 8d31b386d9e..ade1bb2099d 100644
--- a/app/assets/stylesheets/components/rich_content_editor.scss
+++ b/app/assets/stylesheets/components/rich_content_editor.scss
@@ -20,15 +20,15 @@
.tui-popup-wrapper {
@include gl-overflow-hidden;
@include gl-rounded-base;
- @include gl-border-gray-400;
+ @include gl-border-gray-200;
hr {
@include gl-m-0;
- @include gl-bg-gray-400;
+ @include gl-bg-gray-200;
}
button {
- @include gl-text-gray-800;
+ @include gl-text-gray-700;
}
}
diff --git a/app/assets/stylesheets/fontawesome_custom.scss b/app/assets/stylesheets/fontawesome_custom.scss
new file mode 100644
index 00000000000..a2117e9c012
--- /dev/null
+++ b/app/assets/stylesheets/fontawesome_custom.scss
@@ -0,0 +1,332 @@
+/*!
+ * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
+ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+
+// stylelint-disable property-no-vendor-prefix
+// stylelint-disable at-rule-no-vendor-prefix
+// stylelint-disable stylelint-gitlab/duplicate-selectors
+// scss-lint:disable MergeableSelector
+@font-face {
+ font-family: 'FontAwesome';
+ src: asset-url('fontawesome-webfont.woff2?v=4.7.0') format('woff2'), asset-url('fontawesome-webfont.woff?v=4.7.0') format('woff');
+ font-weight: normal;
+ font-style: normal;
+}
+
+.fa {
+ display: inline-block;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* makes the font 33% larger relative to the icon container */
+.fa-lg {
+ font-size: 1.33333333em;
+ line-height: 0.75em;
+ vertical-align: -15%;
+}
+
+.fa-2x {
+ font-size: 2em;
+}
+
+.fa-3x {
+ font-size: 3em;
+}
+
+.fa-4x {
+ font-size: 4em;
+}
+
+.fa-5x {
+ font-size: 5em;
+}
+
+.fa-fw {
+ width: 1.28571429em;
+ text-align: center;
+}
+
+.fa-spin {
+ -webkit-animation: fa-spin 2s infinite linear;
+ animation: fa-spin 2s infinite linear;
+}
+
+@-webkit-keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+
+@keyframes fa-spin {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(359deg);
+ transform: rotate(359deg);
+ }
+}
+
+.fa-inverse {
+ color: $white;
+}
+
+.fa-question-circle::before {
+ content: '\f059';
+}
+
+.fa-chevron-down::before {
+ content: '\f078';
+}
+
+.fa-remove::before,
+.fa-times::before {
+ content: '\f00d';
+}
+
+.fa-caret-down::before {
+ content: '\f0d7';
+}
+
+.fa-check::before {
+ content: '\f00c';
+}
+
+.fa-search::before {
+ content: '\f002';
+}
+
+.fa-warning::before,
+.fa-exclamation-triangle::before {
+ content: '\f071';
+}
+
+.fa-external-link::before {
+ content: '\f08e';
+}
+
+.fa-spinner::before {
+ content: '\f110';
+}
+
+.fa-calendar::before {
+ content: '\f073';
+}
+
+.fa-angle-double-right::before {
+ content: '\f101';
+}
+
+.fa-trash::before {
+ content: '\f1f8';
+}
+
+.fa-angle-double-left::before {
+ content: '\f100';
+}
+
+.fa-arrow-left::before {
+ content: '\f060';
+}
+
+.fa-trash-o::before {
+ content: '\f014';
+}
+
+.fa-caret-right::before {
+ content: '\f0da';
+}
+
+.fa-refresh::before {
+ content: '\f021';
+}
+
+.fa-chevron-up::before {
+ content: '\f077';
+}
+
+.fa-file-text-o::before {
+ content: '\f0f6';
+}
+
+.fa-github::before {
+ content: '\f09b';
+}
+
+.fa-paperclip::before {
+ content: '\f0c6';
+}
+
+.fa-tag::before {
+ content: '\f02b';
+}
+
+.fa-arrow-up::before {
+ content: '\f062';
+}
+
+.fa-bug::before {
+ content: '\f188';
+}
+
+.fa-google::before {
+ content: '\f1a0';
+}
+
+.fa-user::before {
+ content: '\f007';
+}
+
+.fa-exclamation-circle::before {
+ content: '\f06a';
+}
+
+.fa-bell::before {
+ content: '\f0f3';
+}
+
+.fa-arrow-down::before {
+ content: '\f063';
+}
+
+.fa-bitbucket-square::before {
+ content: '\f172';
+}
+
+.fa-file-o::before {
+ content: '\f016';
+}
+
+.fa-users::before {
+ content: '\f0c0';
+}
+
+.fa-tags::before {
+ content: '\f02c';
+}
+
+.fa-lightbulb-o::before {
+ content: '\f0eb';
+}
+
+.fa-circle::before {
+ content: '\f111';
+}
+
+.fa-bitbucket::before {
+ content: '\f171';
+}
+
+.fa-git::before {
+ content: '\f1d3';
+}
+
+.fa-folder::before {
+ content: '\f07b';
+}
+
+.fa-archive::before {
+ content: '\f187';
+}
+
+.fa-thumb-tack::before {
+ content: '\f08d';
+}
+
+.fa-fire::before {
+ content: '\f06d';
+}
+
+.fa-download::before {
+ content: '\f019';
+}
+
+.fa-globe::before {
+ content: '\f0ac';
+}
+
+.fa-pause::before {
+ content: '\f04c';
+}
+
+.fa-play::before {
+ content: '\f04b';
+}
+
+.fa-arrow-right::before {
+ content: '\f061';
+}
+
+.fa-user-secret::before {
+ content: '\f21b';
+}
+
+.fa-search-plus::before {
+ content: '\f00e';
+}
+
+.fa-search-minus::before {
+ content: '\f010';
+}
+
+.fa-share::before {
+ content: '\f064';
+}
+
+.fa-book::before {
+ content: '\f02d';
+}
+
+.fa-times-circle::before {
+ content: '\f057';
+}
+
+.fa-skype::before {
+ content: '\f17e';
+}
+
+.fa-linkedin-square::before {
+ content: '\f08c';
+}
+
+.fa-twitter-square::before {
+ content: '\f081';
+}
+
+.fa-unlink::before {
+ content: '\f127';
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0;
+}
+
+.sr-only-focusable:active,
+.sr-only-focusable:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+}
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 86e701604b5..4f09f1a394b 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -232,7 +232,7 @@
height: $default-icon-size;
width: $default-icon-size;
border-radius: 50%;
- fill: $gray-700;
+ fill: $gray-500;
}
}
diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss
index 5b8a4bf964e..3f1d742ca14 100644
--- a/app/assets/stylesheets/framework/badges.scss
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -1,7 +1,7 @@
.badge.badge-pill:not(.gl-badge) {
font-weight: $gl-font-weight-normal;
background-color: $badge-bg;
- color: $gray-800;
+ color: $gray-700;
vertical-align: baseline;
// Do not use this!
diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss
index f7836213e5c..c1647c16c77 100644
--- a/app/assets/stylesheets/framework/broadcast_messages.scss
+++ b/app/assets/stylesheets/framework/broadcast_messages.scss
@@ -38,7 +38,7 @@
}
.broadcast-message-dismiss {
- color: $gray-800;
+ color: $gray-700;
}
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index fd5b3f74c4a..893a494d240 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -274,8 +274,6 @@
svg {
height: 15px;
width: 15px;
- position: relative;
- top: 2px;
}
svg,
@@ -495,6 +493,10 @@
}
}
+// The .btn-svg class is available for legacy icon buttons to
+// preserve a 34px height and have 16x16 icons at the same time.
+// Once a button is migrated (to the current 32px height)
+// please remove this class from the new button.
.btn-svg svg {
@include btn-svg;
}
diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss
index cae7b9b5e46..2204b037f69 100644
--- a/app/assets/stylesheets/framework/ci_variable_list.scss
+++ b/app/assets/stylesheets/framework/ci_variable_list.scss
@@ -86,7 +86,6 @@
height: $input-height;
padding: 0;
background: transparent;
- border: 0;
color: $gl-text-color-secondary;
&:hover,
@@ -101,7 +100,7 @@
}
.group-variable-list {
- color: $gray-700;
+ color: $gray-500;
.table-section:not(:first-child) {
@include media-breakpoint-down(sm) {
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 1abb7a9c06f..00679cf20fa 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -6,7 +6,7 @@
.cdark { color: $common-gray-dark; }
.fwhite { fill: $white; }
-.fgray { fill: $gray-700; }
+.fgray { fill: $gray-500; }
.text-plain,
.text-plain:hover {
@@ -74,7 +74,7 @@
.hint {
font-style: italic;
- color: $gl-gray-400;
+ color: $gl-gray-200;
}
.light { color: $gl-text-color; }
@@ -168,7 +168,7 @@ table {
}
p.time {
- color: $gl-gray-400;
+ color: $gl-gray-200;
font-size: 90%;
margin: 30px 3px 3px 2px;
}
@@ -396,15 +396,11 @@ img.emoji {
🚨 Do not use these classes — they are deprecated and being removed. 🚨
See https://gitlab.com/gitlab-org/gitlab/-/issues/217418 for more details.
**/
-.prepend-top-10 { margin-top: 10px; }
.prepend-top-15 { margin-top: 15px; }
.prepend-top-20 { margin-top: 20px; }
.prepend-left-15 { margin-left: 15px; }
.prepend-left-20 { margin-left: 20px; }
-.prepend-left-64 { margin-left: 64px; }
-.append-right-15 { margin-right: 15px; }
.append-right-20 { margin-right: 20px; }
-.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-20 { margin-bottom: 20px; }
.ml-10 { margin-left: 4.5rem; }
.inline { display: inline-block; }
@@ -513,7 +509,7 @@ img.emoji {
}
&.is-dragging {
- background-color: $gray-600;
+ background-color: $gray-400;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 32c276ea6d2..6b742853f8f 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -312,6 +312,7 @@
> a,
button,
+ .gl-button.btn-link,
.menu-item {
@include dropdown-link;
}
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
index f9b167669a6..8d411747b28 100644
--- a/app/assets/stylesheets/framework/feature_highlight.scss
+++ b/app/assets/stylesheets/framework/feature_highlight.scss
@@ -29,12 +29,6 @@
}
}
-.is-showing-fly-out {
- .feature-highlight {
- display: none;
- }
-}
-
.feature-highlight-popover-content {
display: none;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 8fd507a45bb..ef7d39a5e7e 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -209,7 +209,7 @@
}
.doc-versions {
- color: $gray-600;
+ color: $gray-400;
&:hover {
color: $gray-900;
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 8f209d2d99a..ed4281123cd 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -134,20 +134,20 @@
padding-left: 8px;
padding-right: 0;
- .fa-close {
+ .close-icon {
color: $gl-text-color-secondary;
}
- &:hover .fa-close {
+ &:hover .close-icon {
color: $gl-text-color;
}
&.inverted {
- .fa-close {
+ .close-icon {
color: $gl-text-color-secondary-inverted;
}
- &:hover .fa-close {
+ &:hover .close-icon {
color: $gl-text-color-inverted;
}
}
@@ -307,23 +307,6 @@
color: $gl-text-color;
border-color: $border-color;
}
-
- svg {
- height: 14px;
- width: 14px;
- vertical-align: middle;
- margin-bottom: 4px;
- }
-
- .dropdown-toggle-text {
- display: inline-block;
- color: inherit;
-
- .fa {
- vertical-align: middle;
- color: inherit;
- }
- }
}
.filtered-search-history-dropdown {
@@ -458,6 +441,23 @@
}
.vue-filtered-search-bar-container {
+ .gl-search-box-by-click {
+ // Absolute width is needed to prevent flex to grow
+ // beyond the available width.
+ .gl-filtered-search-scrollable {
+ width: 1px;
+ }
+
+ // There are several styling issues happening while using
+ // `GlFilteredSearch` in roadmap due to some of our global
+ // styles which we need to override until those are fixed
+ // at framework level.
+ // See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/908
+ .input-group-prepend + .gl-filtered-search-scrollable {
+ border-radius: 0;
+ }
+ }
+
@include media-breakpoint-up(md) {
.sort-dropdown-container {
margin-left: 10px;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index ec8d5806345..7be676ed83c 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -227,7 +227,7 @@ label {
right: 0.8em;
top: 50%;
transform: translateY(-50%);
- color: $gray-600;
+ color: $gray-400;
}
.input-md {
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 288849ba438..97bd6ca6fe2 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -295,9 +295,9 @@ body {
&.ui-dark {
@include gitlab-theme(
$gray-200,
+ $gray-300,
$gray-500,
$gray-700,
- $gray-800,
$gray-900,
$white
);
@@ -305,12 +305,12 @@ body {
&.ui-light {
@include gitlab-theme(
+ $gray-500,
$gray-700,
- $gray-800,
- $gray-700,
- $gray-700,
+ $gray-500,
+ $gray-500,
$gray-50,
- $gray-700
+ $gray-500
);
.navbar-gitlab {
@@ -341,7 +341,7 @@ body {
.container-fluid {
.navbar-toggler,
.navbar-toggler:hover {
- color: $gray-700;
+ color: $gray-500;
border-left: 1px solid $gray-100;
}
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 9ae313db4c1..ec0755b1614 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -34,11 +34,11 @@
.ci-status-icon-preparing {
svg {
- fill: $gray-500;
+ fill: $gray-300;
}
&.add-border {
- @include borderless-status-icon($gray-500);
+ @include borderless-status-icon($gray-300);
}
}
@@ -98,5 +98,5 @@
display: flex;
align-items: center;
justify-content: center;
- color: $gray-700;
+ color: $gray-500;
}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 0fae1c7d235..195a66bf9e5 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -48,4 +48,12 @@ svg {
@include svg-size(#{$svg-size}px);
}
}
+
+ &.s12 {
+ vertical-align: -1px;
+ }
+
+ &.s16 {
+ vertical-align: -3px;
+ }
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 738150dbd2e..9d67b175294 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -132,10 +132,10 @@ ul.content-list {
a {
color: $gl-text-color;
- }
- .member-group-link {
- color: $blue-600;
+ &.inline-link {
+ color: $blue-600;
+ }
}
.description {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 918ca448c21..61e8c0d4718 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -326,8 +326,8 @@
line-height: 1;
padding: 0;
min-width: 16px;
- color: $gray-600;
- fill: $gray-600;
+ color: $gray-400;
+ fill: $gray-400;
.fa {
position: relative;
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index 1878fac1c60..07c3eb19fd4 100644
--- a/app/assets/stylesheets/framework/responsive_tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -20,7 +20,7 @@
@extend .gl-responsive-table-row-layout;
margin-top: 10px;
border: 1px solid $border-color;
- color: $gray-700;
+ color: $gray-500;
&.gl-responsive-table-row-clickable {
&:hover {
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index f85efc63645..1352fa13e1a 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -415,7 +415,3 @@
}
}
}
-
-.new-project-item-select-button .fa-caret-down {
- margin-left: 2px;
-}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index c2ab6f5b8c5..e81ecfb43d5 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -285,7 +285,7 @@
.select2-highlighted {
.group-result {
.group-path {
- color: $gray-800;
+ color: $gray-700;
}
}
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 9b33ed1b630..4ba9db811b7 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,6 +1,5 @@
.content-wrapper {
width: 100%;
- transition: padding $sidebar-transition-duration;
.container-fluid {
padding: 0 $gl-padding;
@@ -13,6 +12,10 @@
}
}
+.page-initialised .content-wrapper {
+ transition: padding $sidebar-transition-duration;
+}
+
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
@@ -168,7 +171,7 @@
&::before {
content: '';
- border-left: 1px solid $gray-500;
+ border-left: 1px solid $gray-300;
position: absolute;
top: $gl-padding;
bottom: $gl-padding;
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index dbcb5086d70..4c1c9d15121 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -32,6 +32,10 @@
.snippet-file-content {
border-radius: 3px;
+
+ + .snippet-file-content {
+ @include gl-mt-5;
+ }
}
.snippet-header {
diff --git a/app/assets/stylesheets/framework/spinner.scss b/app/assets/stylesheets/framework/spinner.scss
index b7a99d421c9..d734895c7dc 100644
--- a/app/assets/stylesheets/framework/spinner.scss
+++ b/app/assets/stylesheets/framework/spinner.scss
@@ -42,7 +42,7 @@
}
&.spinner-dark {
- @include spinner-color($gray-700);
+ @include spinner-color($gray-500);
}
&.spinner-light {
diff --git a/app/assets/stylesheets/framework/stacked_progress_bar.scss b/app/assets/stylesheets/framework/stacked_progress_bar.scss
index 2d16fdf4ee7..a3037549881 100644
--- a/app/assets/stylesheets/framework/stacked_progress_bar.scss
+++ b/app/assets/stylesheets/framework/stacked_progress_bar.scss
@@ -24,7 +24,7 @@
.status-unavailable {
padding: 0 10px;
- color: $gray-700;
+ color: $gray-500;
}
.status-green {
@@ -40,7 +40,7 @@
color: $gl-gray-dark;
&:hover {
- background-color: $gray-300;
+ background-color: $gray-200;
}
}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 5bc2874ea05..1f60485aa36 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -16,7 +16,7 @@ table {
* Remove this code as soon as this happens
*/
&.gl-table {
- @include gl-text-gray-700;
+ @include gl-text-gray-500;
}
&.table {
@@ -60,7 +60,7 @@ table {
}
&.original-gl-th {
- @include gl-text-gray-700;
+ @include gl-text-gray-500;
border-bottom: 1px solid $cycle-analytics-light-gray;
}
}
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
index 8b131f59cfc..054280f3321 100644
--- a/app/assets/stylesheets/framework/toggle.scss
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -31,7 +31,7 @@
height: 24px;
cursor: pointer;
user-select: none;
- background: $gl-gray-400;
+ background: $gray-400;
border-radius: 12px;
padding: 3px;
transition: all 0.4s ease;
@@ -51,26 +51,10 @@
display: block;
left: 0;
border-radius: 9px;
- background: $feature-toggle-color;
+ background: $white;
transition: all 0.2s ease;
-
- &,
- .toggle-icon-svg {
- width: $default-icon-size;
- height: $default-icon-size;
- }
-
- .toggle-icon-svg {
- fill: $gl-gray-400;
- }
-
- .toggle-status-checked {
- display: none;
- }
-
- .toggle-status-unchecked {
- display: inline;
- }
+ width: $default-icon-size;
+ height: $default-icon-size;
}
.loading-icon {
@@ -84,10 +68,6 @@
}
&.is-loading {
- .toggle-icon {
- display: none;
- }
-
.loading-icon {
display: block;
@@ -101,23 +81,22 @@
}
&.is-checked {
- background: $feature-toggle-color-enabled;
+ background: $blue-400;
.toggle-icon {
left: calc(100% - 18px);
+ }
+ }
- .toggle-icon-svg {
- fill: $feature-toggle-color-enabled;
- }
-
- .toggle-status-checked {
- display: inline;
- }
+ &.is-checked .toggle-icon .toggle-status-checked,
+ .toggle-icon .toggle-status-unchecked {
+ display: inline;
+ }
- .toggle-status-unchecked {
- display: none;
- }
- }
+ &.is-checked .toggle-icon .toggle-status-unchecked,
+ &.is-loading .toggle-icon,
+ .toggle-icon .toggle-status-checked {
+ display: none;
}
&.is-disabled {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index b5b86b807a6..8758fe15870 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -89,10 +89,10 @@
background-color: $gray-10;
border-width: 1px;
border-style: solid;
- border-color: $gray-100 $gray-100 $gray-400;
+ border-color: $gray-100 $gray-100 $gray-200;
border-image: none;
border-radius: 3px;
- box-shadow: 0 -1px 0 $gray-400 inset;
+ box-shadow: 0 -1px 0 $gray-200 inset;
}
h1 {
@@ -187,7 +187,7 @@
tr {
th {
- border-bottom: solid 2px $gray-300;
+ border-bottom: solid 2px $gray-200;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 265dceb3c61..69e00f9b2c4 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -165,12 +165,12 @@ $gray-10: #fafafa !default;
$gray-50: #f0f0f0 !default;
$gray-100: #dbdbdb !default;
$gray-200: #bfbfbf !default;
-$gray-300: #ccc !default;
-$gray-400: #bababa !default;
-$gray-500: #a7a7a7 !default;
-$gray-600: #919191 !default;
-$gray-700: #707070 !default;
-$gray-800: #4f4f4f !default;
+$gray-300: #999 !default;
+$gray-400: #868686 !default;
+$gray-500: #666 !default;
+$gray-600: #5e5e5e !default;
+$gray-700: #525252 !default;
+$gray-800: #404040 !default;
$gray-900: #303030 !default;
$gray-950: #1f1f1f !default;
@@ -350,12 +350,12 @@ $gl-font-size-large: 16px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
$gl-text-color: $gray-900;
-$gl-text-color-secondary: $gray-700;
-$gl-text-color-tertiary: $gray-600;
+$gl-text-color-secondary: $gray-500;
+$gl-text-color-tertiary: $gray-400;
$gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: $white;
$gl-text-color-secondary-inverted: rgba($white, 0.85);
-$gl-text-color-disabled: $gray-600;
+$gl-text-color-disabled: $gray-400;
$gl-grayish-blue: #7f8fa4;
$gl-gray-dark: #313236;
$gl-gray-light: #5c5c5c;
@@ -446,6 +446,8 @@ $context-header-height: 60px;
$breadcrumb-min-height: 48px;
$home-panel-title-row-height: 64px;
$home-panel-avatar-mobile-size: 24px;
+$issuable-title-max-width: 350px;
+$milestone-title-max-width: 75px;
$gl-line-height: 16px;
$gl-line-height-18: 18px;
$gl-line-height-20: 20px;
@@ -637,10 +639,13 @@ $issue-boards-card-shadow: rgba(0, 0, 0, 0.1);
They probably should be derived in a smarter way.
*/
$issue-boards-filter-height: 68px;
+$issue-boards-filter-height-md: 110px;
+$issue-boards-filter-height-sm: 299px;
$issue-boards-breadcrumbs-height-xs: 63px;
$issue-board-list-difference-xs: $header-height + $issue-boards-breadcrumbs-height-xs;
$issue-board-list-difference-sm: $header-height + $breadcrumb-min-height;
-$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height;
+$issue-board-list-difference-md: $issue-board-list-difference-sm + $issue-boards-filter-height-md;
+$issue-board-list-difference-lg: $issue-board-list-difference-sm + $issue-boards-filter-height;
/*
The following heights are used in environment_logs.scss and are used for calculation of the log viewer height.
*/
@@ -748,10 +753,6 @@ $login-brand-holder-color: #888;
$project-option-descr-color: #54565b;
$project-network-controls-color: #888;
-$feature-toggle-color: #fff;
-$feature-toggle-text-color: #fff;
-$feature-toggle-color-enabled: #4a8bee;
-
/*
* Monitor Charts
*/
@@ -870,7 +871,7 @@ $popover-max-width: 384px;
$popover-box-shadow: 0 2px 3px 1px $gray-100;
/*
-Issues Analytics
+Issue Analytics
*/
$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
index a8d10ea1a29..dfd7fd355a4 100644
--- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -38,7 +38,7 @@
}
.badge.badge-pill {
- color: var(--ide-text-color, $gray-800);
+ color: var(--ide-text-color, $gray-700);
background-color: var(--ide-background, $badge-bg);
}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index a07755724dd..36587ecde3d 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -366,7 +366,7 @@ $ide-commit-header-height: 48px;
display: block;
margin-left: auto;
margin-right: auto;
- color: var(--ide-text-color-secondary, $gray-700);
+ color: var(--ide-text-color-secondary, $gray-500);
}
.file-status-icon {
@@ -689,7 +689,7 @@ $ide-commit-header-height: 48px;
border-bottom: 1px solid var(--ide-border-color-alt, $white-dark);
svg {
- color: var(--ide-text-color-secondary, $gray-700);
+ color: var(--ide-text-color-secondary, $gray-500);
&:focus,
&:hover {
@@ -721,7 +721,7 @@ $ide-commit-header-height: 48px;
&,
&:hover {
- color: var(--ide-text-color-secondary, $gray-700);
+ color: var(--ide-text-color-secondary, $gray-500);
}
}
@@ -863,9 +863,6 @@ $ide-commit-header-height: 48px;
.ide-external-link {
svg {
display: none;
- position: absolute;
- top: 2px;
- right: -$gl-padding;
}
&:hover,
@@ -1136,7 +1133,7 @@ $ide-commit-header-height: 48px;
.ide-file-icon-holder {
display: flex;
align-items: center;
- color: var(--ide-text-color-secondary, $gray-700);
+ color: var(--ide-text-color-secondary, $gray-500);
}
.file-row:active {
diff --git a/app/assets/stylesheets/pages/alert_management/details.scss b/app/assets/stylesheets/pages/alert_management/details.scss
index 73a4af00c5a..0f889935583 100644
--- a/app/assets/stylesheets/pages/alert_management/details.scss
+++ b/app/assets/stylesheets/pages/alert_management/details.scss
@@ -16,12 +16,12 @@
&:not(:first-child) {
&::before {
- color: $gray-700;
+ color: $gray-500;
font-weight: normal !important;
}
div {
- color: $gray-700;
+ color: $gray-500;
}
}
@@ -35,7 +35,7 @@
}
@include media-breakpoint-down(xs) {
- .alert-details-issue-button {
+ .alert-details-incident-button {
width: 100%;
}
}
diff --git a/app/assets/stylesheets/pages/alert_management/severity-icons.scss b/app/assets/stylesheets/pages/alert_management/severity-icons.scss
index b400e80d5c5..6004697b3e1 100644
--- a/app/assets/stylesheets/pages/alert_management/severity-icons.scss
+++ b/app/assets/stylesheets/pages/alert_management/severity-icons.scss
@@ -1,4 +1,4 @@
-.alert-management-list,
+.incident-management-list,
.alert-management-details {
.icon-critical {
color: $red-800;
@@ -21,6 +21,6 @@
}
.icon-unknown {
- color: $gray-400;
+ color: $gray-200;
}
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 049660220df..51a65b88cd0 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -59,6 +59,10 @@
height: calc(100vh - #{$issue-board-list-difference-md});
}
+ @include media-breakpoint-up(lg) {
+ height: calc(100vh - #{$issue-board-list-difference-lg});
+ }
+
.with-performance-bar & {
height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height});
@@ -69,6 +73,10 @@
@include media-breakpoint-up(md) {
height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height});
}
+
+ @include media-breakpoint-up(lg) {
+ height: calc(100vh - #{$issue-board-list-difference-lg} - #{$performance-bar-height});
+ }
}
}
@@ -191,7 +199,8 @@
align-items: center;
font-size: 1em;
border-bottom: 1px solid $gray-100;
- padding: $gl-padding-8;
+ padding: 0 $gl-spacing-scale-3;
+ height: 3rem;
.js-max-issue-size::before {
content: '/';
@@ -521,7 +530,7 @@
}
&.board-card-weight {
- color: $gl-text-color;
+ color: $gl-text-color-secondary;
cursor: pointer;
&:hover {
@@ -531,8 +540,9 @@
}
.board-card-info-icon {
- color: $gray-600;
+ color: $gray-500;
margin-right: $gl-padding-4;
+ vertical-align: text-top;
}
@include media-breakpoint-down(md) {
@@ -584,3 +594,21 @@
.board-header-collapsed-info-icon:hover {
color: $gray-900;
}
+
+$epic-icons-spacing: 40px;
+
+.board-epic-lane {
+ max-width: calc(100vw - #{$contextual-sidebar-width} - #{$epic-icons-spacing});
+
+ .page-with-icon-sidebar & {
+ max-width: calc(100vw - #{$contextual-sidebar-collapsed-width} - #{$epic-icons-spacing});
+ }
+
+ .page-with-icon-sidebar .is-compact & {
+ max-width: calc(100vw - #{$contextual-sidebar-collapsed-width} - #{$gutter-width} - #{$epic-icons-spacing});
+ }
+
+ .is-compact & {
+ max-width: calc(100vw - #{$contextual-sidebar-width} - #{$gutter-width} - #{$epic-icons-spacing});
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 02c42d5b779..f367d9ea4d8 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -108,7 +108,7 @@
svg {
position: relative;
- top: 5px;
+ top: 3px;
margin-right: 5px;
width: 22px;
height: 22px;
@@ -275,8 +275,6 @@
overflow: auto;
svg {
- position: relative;
- top: 3px;
margin-right: 3px;
height: 14px;
width: 14px;
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 5a69aa15303..29422c1f7fa 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -166,6 +166,6 @@
.cluster-status-indicator {
&.disabled {
- background-color: $gray-600;
+ background-color: $gray-400;
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 9a69afc6044..e6378fd9168 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -388,3 +388,9 @@
display: block;
color: $link-color;
}
+
+.add-review-item {
+ .gl-tab-nav-item {
+ height: 100%;
+ }
+}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index fd5b3ff1dd8..a7b93c9eab7 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -992,7 +992,8 @@ table.code {
}
.frame .badge.badge-pill,
-.frame .image-comment-badge {
+.frame .image-comment-badge,
+.frame .comment-indicator {
// Center align badges on the frame
transform: translate(-50%, -50%);
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index fd11d0e3a69..9f9964ac447 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -16,6 +16,11 @@
padding: 0;
position: relative;
width: 100%;
+
+ .editor-loading-content {
+ height: 100%;
+ border: 0;
+ }
}
.ace_gutter-cell {
@@ -160,9 +165,8 @@
vertical-align: top;
@media(max-width: map-get($grid-breakpoints, lg)-1) {
- display: block;
+ display: inline-block;
width: 100%;
- margin: 5px 0;
padding: 0;
border-left: 0;
}
@@ -182,7 +186,8 @@
.gitignore-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector,
- .template-type-selector {
+ .template-type-selector,
+ .metrics-dashboard-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index ab6716903c7..ef7b56ac210 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -98,19 +98,10 @@
}
}
-.refresh-dashboard-button {
- margin-top: 22px;
-
- @media(max-width: map-get($grid-breakpoints, sm)) {
- margin-top: 0;
- }
-}
-
.metric-area {
opacity: 0.25;
}
-
.rect-text-metric {
fill: $white;
stroke-width: 1;
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index f9beb6fe037..c4b6cdd703d 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -49,7 +49,7 @@
text {
font-weight: bold;
font-size: 12px;
- fill: $gray-800;
+ fill: $gray-700;
}
}
}
@@ -70,5 +70,5 @@
.animate-flicker {
animation: flickerAnimation 1.5s infinite;
- fill: $gray-500;
+ fill: $gray-300;
}
diff --git a/app/assets/stylesheets/pages/alert_management/list.scss b/app/assets/stylesheets/pages/incident_management_list.scss
index e420209b1fc..00ca3cc73e0 100644
--- a/app/assets/stylesheets/pages/alert_management/list.scss
+++ b/app/assets/stylesheets/pages/incident_management_list.scss
@@ -1,11 +1,11 @@
-.alert-management-list {
+.incident-management-list {
.new-alert {
background-color: $issues-today-bg;
}
// these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui
table {
- color: $gray-700;
+ @include gl-text-gray-500;
tr {
&:focus {
@@ -14,16 +14,15 @@
td,
th {
- @include gl-pl-9;
@include gl-py-5;
@include gl-outline-none;
@include gl-relative;
}
th {
- background-color: transparent;
- font-weight: $gl-font-weight-bold;
- color: $gl-gray-600;
+ @include gl-bg-transparent;
+ @include gl-font-weight-bold;
+ @include gl-text-gray-400;
&[aria-sort='none']:hover {
background-image: url('data:image/svg+xml, %3csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="4 0 8 16"%3e %3cpath style="fill: %23BABABA;" fill-rule="evenodd" d="M11.707085,11.7071 L7.999975,15.4142 L4.292875,11.7071 C3.902375,11.3166 3.902375, 10.6834 4.292875,10.2929 C4.683375,9.90237 5.316575,9.90237 5.707075,10.2929 L6.999975, 11.5858 L6.999975,2 C6.999975,1.44771 7.447695,1 7.999975,1 C8.552255,1 8.999975,1.44771 8.999975,2 L8.999975,11.5858 L10.292865,10.2929 C10.683395 ,9.90237 11.316555,9.90237 11.707085,10.2929 C12.097605,10.6834 12.097605,11.3166 11.707085,11.7071 Z"/%3e %3c/svg%3e');
@@ -39,19 +38,37 @@
}
}
}
+
+ .sortable-cell {
+ padding-left: calc(0.75rem + 0.65em);
+ }
}
}
@include media-breakpoint-down(sm) {
- .alert-management-table {
+ table {
tr {
- border-top: 0;
+ @include gl-border-t-0;
.table-col {
min-height: 68px;
+ }
+
+ &:hover {
+ @include gl-bg-white;
+ @include gl-border-none;
+ }
+
+ th,
+ td {
+ @include gl-pt-6;
+ }
+ }
+ &.alert-management-table {
+ .table-col {
&:last-child {
- background-color: $gray-10;
+ @include gl-bg-gray-10;
&::before {
content: none !important;
@@ -63,23 +80,56 @@
}
}
}
+ }
- &:hover {
- background-color: $white;
- border-color: $white;
- border-bottom-style: none;
+ .b-table-empty-row {
+ td {
+ @include gl-border-b-0;
+
+ div {
+ text-align: unset !important;
+ }
}
}
+
+ .b-table-busy-slot {
+ td {
+ @include gl-border-b-0;
+
+ div {
+ text-align: center !important;
+ }
+ }
+ }
+ }
+ }
+
+ .gl-tabs-nav {
+ border-bottom-width: 0;
+
+ .gl-tab-nav-item {
+ color: $gl-gray-600;
+
+ > .gl-tab-counter-badge {
+ color: inherit;
+ @include gl-font-sm;
+ background-color: $gray-50;
+ }
}
}
- .gl-tab-nav-item {
- color: $gl-gray-600;
+ @include media-breakpoint-down(xs) {
+ .incident-management-list-header {
+ flex-direction: column-reverse;
+ }
- > .gl-tab-counter-badge {
- color: inherit;
- @include gl-font-sm;
- background-color: $white-normal;
+ .create-incident-button {
+ @include gl-w-full;
}
}
+
+ // TODO: Abstract to `@gitlab/ui` utility set: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/921
+ .gl-fill-green-500 {
+ fill: $green-500;
+ }
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index a7d0d4259ea..2f28361f62c 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -166,6 +166,14 @@
color: inherit;
}
+ // TODO remove this class once we can generate a correct hover utility from `gitlab/ui`,
+ // see here: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39286#note_396767000
+ .btn-link-hover:hover {
+ * {
+ @include gl-text-blue-800;
+ }
+ }
+
.issuable-header-text {
margin-top: 7px;
}
@@ -598,18 +606,18 @@
padding: 16px 0;
small {
- color: $gray-700;
+ color: $gray-500;
}
}
.edited-text {
- color: $gray-700;
+ color: $gray-500;
display: block;
margin: 16px 0 0;
font-size: 85%;
.author-link {
- color: $gray-700;
+ color: $gray-500;
}
}
@@ -804,6 +812,10 @@
}
}
}
+
+ .milestone {
+ color: $gray-700;
+ }
}
@media(max-width: map-get($grid-breakpoints, lg)-1) {
@@ -956,7 +968,7 @@
.sidebar-collapsed-divider {
line-height: 5px;
font-size: 12px;
- color: $gray-700;
+ color: $gray-500;
+ .sidebar-collapsed-icon {
padding-top: 0;
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 73d2c3ca2f8..e37b26187e7 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -336,11 +336,11 @@
}
.label-action {
- color: $gray-800;
+ color: $gray-700;
cursor: pointer;
svg {
- fill: $gray-800;
+ fill: $gray-700;
}
&:hover {
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 54bca80194f..2d9a9f3029f 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -180,10 +180,6 @@
word-break: break-all;
}
- .member-group-link {
- display: inline-block;
- }
-
.form-control {
width: inherit;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 5cf2d847405..a6d1fc11c3f 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -760,11 +760,6 @@ $mr-widget-min-height: 69px;
color: $gl-text-color;
}
- .fa-info-circle {
- color: $orange-500;
- padding-right: 5px;
- }
-
// Shortening button height by 1px to make compare-versions
// header 56px and fit into our 8px design grid
button {
@@ -1010,7 +1005,7 @@ $mr-widget-min-height: 69px;
.coverage {
font-size: 12px;
- color: $gray-700;
+ color: $gray-500;
line-height: initial;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3a210d66420..35a15214f68 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -152,7 +152,7 @@
}
.sidebar-item-value & {
- fill: $gray-700;
+ fill: $gray-500;
}
}
@@ -282,7 +282,7 @@ table {
display: table;
svg {
- fill: $gray-700;
+ fill: $gray-500;
}
.btn-group {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 40f0104a2bf..e4e54501627 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -674,7 +674,7 @@ $note-form-margin-left: 72px;
.note-headline-meta {
.system-note-separator {
- color: $gray-700;
+ color: $gray-500;
}
.note-timestamp {
@@ -727,7 +727,7 @@ $note-form-margin-left: 72px;
display: inline-flex;
align-items: center;
margin-left: 10px;
- color: $gray-600;
+ color: $gray-400;
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
float: none;
@@ -820,9 +820,7 @@ $note-form-margin-left: 72px;
}
}
-.add-diff-note {
- @include btn-comment-icon;
- opacity: 0;
+.tooltip-wrapper.add-diff-note {
margin-left: -52px;
position: absolute;
top: 50%;
@@ -830,6 +828,18 @@ $note-form-margin-left: 72px;
z-index: 10;
}
+.note-button.add-diff-note {
+ @include btn-comment-icon;
+ opacity: 0;
+
+ &[disabled] {
+ background: $white;
+ border-color: $gray-200;
+ color: $gl-gray-400;
+ cursor: not-allowed;
+ }
+}
+
.disabled-comment {
background-color: $gray-light;
border-radius: $border-radius-base;
@@ -867,7 +877,7 @@ $note-form-margin-left: 72px;
line-height: $gl-line-height;
svg {
- fill: $gray-700;
+ fill: $gray-500;
}
&.discussion-create-issue-btn {
@@ -904,7 +914,7 @@ $note-form-margin-left: 72px;
border-right: 0;
.line-resolve-btn {
- color: $gray-700;
+ color: $gray-500;
svg {
vertical-align: text-top;
@@ -989,11 +999,6 @@ $note-form-margin-left: 72px;
}
.discussion-filter-container {
- .btn > svg {
- width: $gl-col-padding;
- height: $gl-col-padding;
- }
-
.dropdown-menu {
margin-bottom: $gl-padding-4;
diff --git a/app/assets/stylesheets/pages/packages.scss b/app/assets/stylesheets/pages/packages.scss
new file mode 100644
index 00000000000..8f6eee524e5
--- /dev/null
+++ b/app/assets/stylesheets/pages/packages.scss
@@ -0,0 +1,11 @@
+.commit-row-description {
+ border: 0;
+ border-left: 3px solid $white-dark;
+}
+
+.package-list-table[aria-busy='true'] {
+ td {
+ padding-bottom: 0;
+ padding-top: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 57ad9abef4b..fc3b786b365 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -445,6 +445,7 @@
.pipeline-tab-content {
display: flex;
width: 100%;
+ min-height: $dropdown-max-height-lg;
background-color: $gray-light;
padding: $gl-padding 0;
overflow: auto;
@@ -669,24 +670,13 @@
&.ci-action-icon-wrapper {
height: 30px;
width: 30px;
- background: $white;
- border: 1px solid $border-color;
border-radius: 100%;
display: block;
-
- &:hover {
- background-color: $gray-darker;
- border: 1px solid $dropdown-toggle-active-border-color;
-
- svg {
- fill: $gl-text-color;
- }
- }
+ padding: 0;
+ line-height: 0;
svg {
fill: $gl-text-color-secondary;
- position: relative;
- top: -1px;
}
.spinner {
@@ -695,7 +685,8 @@
&.play {
svg {
- left: 2px;
+ left: 1px;
+ top: 1px;
}
}
}
@@ -804,12 +795,12 @@
&.ci-status-icon-disabled,
&.ci-status-icon-not-found,
&.ci-status-icon-manual {
- @include mini-pipeline-graph-color($white, $gray-700, $gray-800, $gray-900, $gray-950, $black);
+ @include mini-pipeline-graph-color($white, $gray-500, $gray-700, $gray-900, $gray-950, $black);
}
&.ci-status-icon-created,
&.ci-status-icon-skipped {
- @include mini-pipeline-graph-color($white, $gray-100, $gray-300, $gray-500, $gray-600, $gray-700);
+ @include mini-pipeline-graph-color($white, $gray-100, $gray-200, $gray-300, $gray-400, $gray-500);
}
}
@@ -845,15 +836,12 @@ button.mini-pipeline-graph-dropdown-toggle {
&.ci-action-icon-wrapper {
height: $ci-action-dropdown-button-size;
width: $ci-action-dropdown-button-size;
-
- background: $white;
- border: 1px solid $border-color;
border-radius: 50%;
display: block;
&:hover {
+ box-shadow: inset 0 0 0 0.0625rem $dropdown-toggle-active-border-color;
background-color: $gray-darker;
- border: 1px solid $dropdown-toggle-active-border-color;
svg {
fill: $gl-text-color;
@@ -866,7 +854,7 @@ button.mini-pipeline-graph-dropdown-toggle {
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
position: relative;
- top: auto;
+ top: 1px;
vertical-align: initial;
}
}
@@ -874,7 +862,7 @@ button.mini-pipeline-graph-dropdown-toggle {
// SVGs in the commit widget and mr widget
a.ci-action-icon-container.ci-action-icon-wrapper svg {
- top: 4px;
+ top: 5px;
}
.scrollable-menu {
@@ -1052,13 +1040,6 @@ button.mini-pipeline-graph-dropdown-toggle {
.text-center {
padding-top: 12px;
}
-
- .header-action-buttons {
- .btn,
- a {
- margin-left: 10px;
- }
- }
}
.pipelines-container .top-area .nav-controls > .btn:last-child {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 438f6c2546e..d4d6583312c 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -143,8 +143,8 @@
.group-home-panel,
.project-home-panel {
- padding-top: $gl-padding;
- padding-bottom: $gl-padding;
+ margin-top: $gl-padding;
+ margin-bottom: $gl-padding;
.home-panel-avatar {
width: $home-panel-title-row-height;
@@ -159,7 +159,7 @@
font-weight: bold;
.icon {
- font-size: $gl-font-size-large;
+ vertical-align: -1px;
}
.home-panel-topic-list {
@@ -224,12 +224,6 @@
font-size: $gl-font-size-large;
}
}
-
- .notifications-btn {
- .fa-bell {
- margin-right: 0;
- }
- }
}
.nav > .project-buttons {
@@ -472,17 +466,9 @@
margin-right: auto;
}
- a {
- display: block;
- width: 100%;
- height: 100%;
- padding-top: $gl-padding;
- text-decoration: none;
-
- &.disabled {
- opacity: 0.3;
- cursor: not-allowed;
- }
+ a.disabled {
+ opacity: 0.3;
+ cursor: not-allowed;
}
}
@@ -839,7 +825,7 @@
}
.repository-language-bar-tooltip-share {
- color: $gray-400;
+ color: $gray-200;
}
pre.light-well {
@@ -1538,3 +1524,10 @@ 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/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index 6461d09bb47..a3b6cbdff25 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -48,7 +48,7 @@
&.sortable-chosen .draggable-panel {
background: $white;
- box-shadow: 0 0 4px $gray-500;
+ box-shadow: 0 0 4px $gray-300;
}
.draggable-remove {
@@ -56,18 +56,13 @@
.draggable-remove-link {
cursor: pointer;
- color: $gray-600;
+ color: $gray-400;
background-color: $white;
}
}
}
.prometheus-graphs-header {
- .monitor-environment-dropdown-header header,
- .monitor-dashboard-dropdown-header header {
- font-size: $gl-font-size;
- }
-
.monitor-environment-dropdown-menu,
.monitor-dashboard-dropdown-menu {
&.show {
@@ -117,7 +112,7 @@
.prometheus-graph-cursor {
position: absolute;
- background: $gray-600;
+ background: $gray-400;
width: 1px;
}
@@ -290,7 +285,7 @@
}
> text {
- fill: $gray-600;
+ fill: $gray-400;
font-size: 10px;
}
}
@@ -341,3 +336,11 @@
opacity: 0;
pointer-events: all;
}
+
+.prometheus-panel-builder {
+ .preview-date-time-picker {
+ // same as in .dropdown-menu-toggle
+ // see app/assets/stylesheets/framework/dropdowns.scss
+ width: 160px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss
index 56194f0af67..0c0605b0b3d 100644
--- a/app/assets/stylesheets/pages/reports.scss
+++ b/app/assets/stylesheets/pages/reports.scss
@@ -77,7 +77,7 @@
}
&.neutral svg {
- color: $gray-700;
+ color: $gray-500;
}
.ci-status-icon {
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 66d2f76c558..8ed6936475b 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -44,10 +44,6 @@
.btn-default {
color: $gl-text-color-secondary;
}
-
- .fa-pause {
- font-size: 11px;
- }
}
@include media-breakpoint-down(md) {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index d8084a20af9..1fc6ad62237 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -249,7 +249,7 @@ input[type='checkbox']:hover {
.search-clear {
position: absolute;
right: 10px;
- top: 10px;
+ top: 9px;
padding: 0;
color: $gray-darkest;
line-height: 0;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index f1df9099d82..b82c638a5b7 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -183,7 +183,7 @@
.option-description,
.option-disabled-reason {
- margin-left: 30px;
+ margin-left: 20px;
color: $project-option-descr-color;
margin-top: -5px;
}
@@ -366,7 +366,8 @@
margin-top: 1em;
}
-.ci-variable-table {
+.ci-variable-table,
+.deploy-freeze-table {
table {
thead {
border-bottom: 1px solid $white-normal;
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 4f3d6fb0d44..4ad2dcbe92f 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -12,8 +12,6 @@
svg {
height: 13px;
width: 13px;
- position: relative;
- top: 2px;
overflow: visible;
}
@@ -38,7 +36,7 @@
}
&.ci-preparing {
- @include status-color($gray-100, $gray-500, $gray-600);
+ @include status-color($gray-100, $gray-300, $gray-400);
}
&.ci-pending,
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index bbb37479fb0..c6f104a024b 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -143,6 +143,10 @@
margin-bottom: 0;
}
}
+
+ .gl-label-scoped {
+ --label-inset-border: inset 0 0 0 1px currentColor;
+ }
}
@include media-breakpoint-down(sm) {
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 22b5859e297..b6af395a802 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -138,12 +138,6 @@
}
.tree-item {
- .file-icon,
- .folder-icon {
- position: relative;
- top: 2px;
- }
-
.link-container {
padding: 0;
diff --git a/app/assets/stylesheets/pages/users.scss b/app/assets/stylesheets/pages/users.scss
index 3b018c1e087..0863b573f75 100644
--- a/app/assets/stylesheets/pages/users.scss
+++ b/app/assets/stylesheets/pages/users.scss
@@ -25,8 +25,12 @@
}
.form-control {
- width: 100%;
padding-right: 35px;
+ }
+
+ .search-control-wrap,
+ .form-control {
+ width: 100%;
@include media-breakpoint-up(sm) {
width: 250px;
diff --git a/app/assets/stylesheets/startup/_cloaking.scss b/app/assets/stylesheets/startup/_cloaking.scss
new file mode 100644
index 00000000000..3c25feb0c5c
--- /dev/null
+++ b/app/assets/stylesheets/startup/_cloaking.scss
@@ -0,0 +1,13 @@
+/**
+ Prevent flashing of content when using startup.css
+ */
+@mixin cloak-startup-scss($display) {
+ // Breadcrumbs and alerts on the top of the page
+ .content-wrapper > .alert-wrapper,
+ // Content on pages
+ #content-body,
+ // Prevent flashing of haml generated modal contents
+ .modal-dialog {
+ display: $display;
+ }
+}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
new file mode 100644
index 00000000000..2a7a9255ded
--- /dev/null
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -0,0 +1,5 @@
+@charset "UTF-8";*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;overflow-y:scroll}header,nav{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans",Ubuntu,Cantarell,"Helvetica Neue",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-weight:400;line-height:1.5;color:#303030;text-align:left;background-color:#fff}hr{box-sizing:content-box;height:0;margin-top:.5rem;margin-bottom:.5rem;border:0;border-top:1px solid rgba(0,0,0,.1);overflow:hidden;margin:24px 0;border-top:1px solid #eee}p,ul{margin-top:0;margin-bottom:1rem}ul ul{margin-bottom:0}strong{font-weight:700}a{text-decoration:none;background-color:transparent;color:#1068bf}a:not([href]){color:inherit;text-decoration:none}code{font-family:"Menlo","DejaVu Sans Mono","Liberation Mono","Consolas","Ubuntu Mono","Courier New","andale mono","lucida console",monospace;font-size:90%;word-wrap:break-word;padding:2px 4px;color:#1f1f1f;background-color:#f0f0f0;border-radius:4px}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:baseline;fill:currentColor}button{border-radius:0;text-transform:none}button,input{margin:0;font-family:inherit;font-size:inherit;line-height:inherit;overflow:visible}[type=button]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}[type=search]{outline-offset:-2px}summary{display:list-item;cursor:pointer}[hidden]{display:none!important}.h1,h1{margin-bottom:.25rem;font-weight:600;line-height:1.2;color:#303030;font-size:2.1875rem}.list-unstyled{padding-left:0;list-style:none}a>code{color:inherit}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.search form{display:block;padding:.375rem .75rem;font-weight:400;color:#303030;background-color:#fff;background-clip:padding-box;border-radius:.25rem}.search form::-ms-expand{background-color:transparent;border:0}.search form:-moz-focusring{color:transparent;text-shadow:0 0 0 #303030}.search form::placeholder{opacity:1;color:#919191}.search form:disabled{background-color:#fafafa;opacity:1}.form-inline{display:flex;flex-flow:row wrap;align-items:center}@media (min-width:576px){.form-inline .search form,.search .form-inline form{display:inline-block;width:auto;vertical-align:middle}}.btn{display:inline-block;text-align:center;vertical-align:middle;cursor:pointer;user-select:none;border:1px solid transparent;padding:.375rem .75rem;line-height:20px;border-radius:.25rem}.btn:disabled{opacity:.65}.btn-success{color:#fff;background-color:#108548;border-color:#108548}.btn-success:disabled{color:#fff;background-color:#108548;border-color:#108548}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-menu-toggle{color:#fff;background-color:#0b572f;border-color:#094c29}.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.collapse:not(.show){display:none}.dropdown-menu-toggle::after{margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-menu-toggle:empty::after{margin-left:0}.dropdown-menu{left:0;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#303030;text-align:left;list-style:none;background-clip:padding-box;border:1px solid rgba(0,0,0,.15)}.dropdown-menu-right{right:0;left:auto}.divider{height:0;margin:4px 0;overflow:hidden;border-top:1px solid #dbdbdb}.dropdown-menu.show{display:block}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.navbar{position:relative;padding:.25rem .5rem}.navbar,.navbar .container,.navbar .container-fluid{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .dropdown-menu{float:none}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}.badge,.card{border-radius:.25rem}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid #dbdbdb}.card>hr{margin-right:0;margin-left:0}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:600;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.close{float:right;font-size:1.5rem;font-weight:600;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}button.close{padding:0;background-color:transparent;border:0;appearance:none}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dbdbdb!important}.rounded{border-radius:.25rem!important}.d-none{display:none!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}@media (min-width:576px){.d-sm-none{display:none!important}}@media (min-width:768px){.d-md-block{display:block!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-block{display:block!important}}@media (min-width:1200px){.d-xl-block{display:block!important}}.float-right{float:right!important}.sr-only{white-space:nowrap}.m-auto{margin:auto!important}.text-nowrap{white-space:nowrap!important}.search form,body{font-size:.875rem}[role=button],button,html [type=button]{cursor:pointer}.h1,h1{margin-top:20px;margin-bottom:10px}input[type=file]{line-height:1}.code>code{background-color:inherit;padding:unset}.hidden{display:none!important;visibility:hidden!important}.dropdown-menu-toggle::after,.hide{display:none}.badge:not(.gl-badge){padding:4px 5px;font-size:12px;font-style:normal;font-weight:400;display:inline-block}.toggle-sidebar-button .collapse-text,.toggle-sidebar-button .icon-chevron-double-lg-left,.toggle-sidebar-button .icon-chevron-double-lg-right{color:#707070}body{text-decoration-skip:ink}.container{padding-top:0;z-index:5}.container .content{margin:0}@media (max-width:575.98px){.container .content{margin-top:20px}.container .container .title{padding-left:15px!important}}.btn{border-radius:4px;font-size:.875rem;font-weight:400;padding:6px 10px;background-color:#fff;border-color:#dbdbdb;color:#303030;white-space:nowrap}.btn:active{box-shadow:none}.btn.active,.btn:active{box-shadow:rgba(0,0,0,.16);background-color:#eaeaea;border-color:#e3e3e3;color:#303030}.btn.btn-sm{padding:4px 10px;font-size:13px;line-height:18px}.btn.btn-success{background-color:#108548;border-color:#217645;color:#fff}.btn.btn-success.active,.btn.btn-success:active{box-shadow:rgba(0,0,0,.16);background-color:#24663b;border-color:#0d532a;color:#fff}.btn svg{height:15px;width:15px;position:relative;top:2px}.btn .fa:not(:last-child),.btn svg:not(:last-child){margin-right:5px}.badge.badge-pill:not(.gl-badge){font-weight:400;background-color:rgba(0,0,0,.07);color:#4f4f4f;vertical-align:baseline}.loading{margin:20px auto;height:40px;color:#555;font-size:32px;text-align:center}.chart{overflow:hidden;height:220px}.center{text-align:center}.flex{display:flex}.dropdown{position:relative}.show.dropdown .dropdown-menu{transform:translateY(0);display:block;min-height:40px;max-height:312px;overflow-y:auto}@media (max-width:575.98px){.show.dropdown .dropdown-menu{width:100%}}.show.dropdown .dropdown-menu-toggle{border-color:#c4c4c4}.search-input-container .dropdown-menu{margin-top:11px}.dropdown-menu,.dropdown-menu-toggle{font-size:14px;background-color:#fff;border:1px solid #dbdbdb;border-radius:.25rem}.dropdown-menu-toggle{color:#303030;text-align:left;white-space:nowrap;padding:6px 25px 6px 10px;position:relative;width:160px;text-overflow:ellipsis;overflow:hidden}.no-outline.dropdown-menu-toggle,.show.dropdown [data-toggle=dropdown]{outline:0}.dropdown-menu-toggle .fa{color:#c4c4c4;position:absolute}.dropdown-menu{display:none;position:absolute;width:auto;top:100%;z-index:300;min-width:240px;max-width:500px;margin-top:4px;margin-bottom:24px;font-weight:400;padding:8px 0;box-shadow:0 2px 4px rgba(0,0,0,.1)}.dropdown-menu ul{margin:0;padding:0}.dropdown-menu li{display:block;text-align:left;list-style:none;padding:0 1px}.dropdown-menu li button,.dropdown-menu li>a{background:0 0;border:0;border-radius:0;box-shadow:none;display:block;font-weight:400;position:relative;padding:8px 12px;color:#303030;line-height:16px;white-space:normal;overflow:hidden;text-align:left;width:100%}.dropdown-menu li button:active,.dropdown-menu li>a:active{background-color:#eee;color:#303030;outline:0;text-decoration:none}.dropdown-menu li button:active .avatar,.dropdown-menu li>a:active .avatar{border-color:#fff}.dropdown-menu li button:active .badge.badge-pill,.dropdown-menu li>a:active .badge.badge-pill{background-color:#d3e7f9}.dropdown-menu .divider{height:1px;margin:.25rem 0;padding:0;background-color:#dbdbdb}.dropdown-menu .badge.badge-pill+span:not(.badge.badge-pill){margin-right:40px}.dropdown-select{width:300px}@media (max-width:767.98px){.dropdown-select{width:100%}}.dropdown-content{max-height:252px;overflow-y:auto}.dropdown-loading{position:absolute;top:0;right:0;bottom:0;left:0;display:none;z-index:9;background-color:rgba(255,255,255,.6);font-size:28px}.dropdown-loading .fa{position:absolute;top:50%;left:50%;margin-top:-14px;margin-left:-14px}@media (max-width:575.98px){.navbar-gitlab li.dropdown{position:static}header.navbar-gitlab .dropdown .dropdown-menu{width:100%;min-width:100%}}@media (max-width:767.98px){.dropdown-menu-toggle{width:100%}}input{border-radius:.25rem;color:#303030;background-color:#fff}.search form{margin:0;padding:4px;width:200px;line-height:24px;height:32px;border:0;border-radius:4px}body.ui-indigo .navbar-gitlab{background-color:#292961}body.ui-indigo .navbar-gitlab .nav>li,body.ui-indigo .navbar-gitlab .navbar-collapse,body.ui-indigo .navbar-gitlab .navbar-sub-nav{color:#d1d1f0}body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler{border-left:1px solid #6868b9}body.ui-indigo .navbar-gitlab .container-fluid .navbar-toggler svg{fill:#d1d1f0}body.ui-indigo .navbar-gitlab .nav>li.active>a,body.ui-indigo .navbar-gitlab .nav>li.dropdown.show>a,body.ui-indigo .navbar-gitlab .navbar-nav>li.active>a,body.ui-indigo .navbar-gitlab .navbar-nav>li.active>button,body.ui-indigo .navbar-gitlab .navbar-nav>li.dropdown.show>a,body.ui-indigo .navbar-gitlab .navbar-nav>li.dropdown.show>button,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.active>a,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.active>button,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.dropdown.show>a,body.ui-indigo .navbar-gitlab .navbar-sub-nav>li.dropdown.show>button{color:#292961;background-color:#fff}body.ui-indigo .navbar-gitlab .nav>li>a.header-user-dropdown-toggle .header-user-avatar{border-color:#d1d1f0}body.ui-indigo .search form{background-color:rgba(209,209,240,.2)}body.ui-indigo .search .search-input::placeholder{color:rgba(209,209,240,.8)}body.ui-indigo .search .search-input-wrap .clear-icon,body.ui-indigo .search .search-input-wrap .search-icon{fill:rgba(209,209,240,.8)}body.ui-indigo .nav-sidebar li.active{box-shadow:inset 4px 0 0 #4b4ba3}body.ui-indigo .nav-sidebar li.active>a,body.ui-indigo .sidebar-top-level-items>li.active .badge.badge-pill{color:#393982}body.ui-indigo .nav-sidebar li.active .nav-icon-container svg{fill:#393982}.navbar-gitlab{padding:0 16px;z-index:1000;margin-bottom:0;min-height:40px;border:0;border-bottom:1px solid #dbdbdb;position:fixed;top:0;left:0;right:0;border-radius:0}.navbar-gitlab .logo-text{line-height:initial}.navbar-gitlab .logo-text svg{width:55px;height:14px;margin:0;fill:#fff}.navbar-gitlab .close-icon{display:none}.navbar-gitlab .header-content{width:100%;display:flex;justify-content:space-between;position:relative;min-height:40px;padding-left:0}.navbar-gitlab .header-content .title-container{display:flex;align-items:stretch;flex:1 1 auto;padding-top:0;overflow:visible}.navbar-gitlab .header-content .title{padding-right:0;color:currentColor;display:flex;position:relative;margin:0;font-size:18px;vertical-align:top;white-space:nowrap}.navbar-gitlab .header-content .title img{height:28px}.navbar-gitlab .header-content .title img+.logo-text{margin-left:8px}.navbar-gitlab .header-content .title a{display:flex;align-items:center;padding:2px 8px;margin:5px 2px 5px -8px;border-radius:4px}.navbar-gitlab .header-content .dropdown.open>a{border-bottom-color:#fff}.navbar-gitlab .header-content .navbar-collapse>ul.nav>li:not(.d-none){margin:0 2px}.navbar-gitlab .navbar-collapse{flex:0 0 auto;border-top:0;padding:0}@media (max-width:575.98px){.navbar-gitlab .navbar-collapse{flex:1 1 auto}}.navbar-gitlab .navbar-collapse .nav{flex-wrap:nowrap}@media (max-width:575.98px){.navbar-gitlab .navbar-collapse .nav>li:not(.d-none) a{margin-left:0}}.navbar-gitlab .container-fluid{padding:0}.navbar-gitlab .container-fluid .user-counter svg{margin-right:3px}.navbar-gitlab .container-fluid .navbar-toggler{position:relative;right:-10px;border-radius:0;min-width:45px;padding:0;margin:8px -7px 8px 0;font-size:14px;text-align:center;color:currentColor}.navbar-gitlab .container-fluid .navbar-toggler.active{color:currentColor;background-color:transparent}@media (max-width:575.98px){.navbar-gitlab .container-fluid .navbar-nav{display:flex;padding-right:10px;flex-direction:row}}.navbar-gitlab .container-fluid .navbar-nav li .badge.badge-pill{box-shadow:none;font-weight:600}@media (max-width:575.98px){.navbar-gitlab .container-fluid .nav>li.header-user{padding-left:10px}}.navbar-gitlab .container-fluid .nav>li>a{will-change:color;margin:4px 0;padding:6px 8px;height:32px}@media (max-width:575.98px){.navbar-gitlab .container-fluid .nav>li>a{padding:0}}.navbar-gitlab .container-fluid .nav>li>a.header-user-dropdown-toggle{margin-left:2px}.navbar-gitlab .container-fluid .nav>li .header-new-dropdown-toggle,.navbar-gitlab .container-fluid .nav>li>a.header-user-dropdown-toggle .header-user-avatar{margin-right:0}.navbar-nav>li>a,.navbar-nav>li>button,.navbar-sub-nav>li>a,.navbar-sub-nav>li>button{display:flex;align-items:center;justify-content:center;padding:6px 8px;margin:4px 2px;font-size:12px;color:currentColor;border-radius:4px;height:32px;font-weight:600}.navbar-nav>li>button,.navbar-sub-nav>li>button{background:0 0;border:0}.navbar-nav .dropdown-menu,.navbar-sub-nav .dropdown-menu{position:absolute}.navbar-sub-nav{display:flex;margin:0 0 0 6px}.btn .caret-down,.caret-down{top:0;height:11px;width:11px;margin-left:4px;fill:currentColor}.header-new .dropdown-menu,.header-user .dropdown-menu{margin-top:4px}.btn-sign-in{background-color:#ebebfa;color:#292961;font-weight:600;line-height:18px;margin:4px 0 4px 2px}.navbar-nav .badge.badge-pill,.title-container .badge.badge-pill{position:inherit;font-weight:400;margin-left:-6px;font-size:11px;color:#fff;padding:0 5px;line-height:12px;border-radius:7px;box-shadow:0 1px 0 rgba(76,78,84,.2)}.navbar-nav .badge.badge-pill.green-badge,.title-container .badge.badge-pill.green-badge{background-color:#108548}.navbar-nav .badge.badge-pill.merge-requests-count,.title-container .badge.badge-pill.merge-requests-count{background-color:#de7e00}.navbar-nav .badge.badge-pill.todos-count,.title-container .badge.badge-pill.todos-count{background-color:#1f75cb}.navbar-nav .canary-badge .badge,.title-container .canary-badge .badge{font-size:12px;line-height:16px;padding:0 .5rem}@media (max-width:575.98px){.navbar-gitlab .container-fluid{font-size:18px}.navbar-gitlab .container-fluid .navbar-nav{table-layout:fixed;width:100%;margin:0;text-align:right}.navbar-gitlab .container-fluid .navbar-collapse{margin-left:-8px;margin-right:-10px}.navbar-gitlab .container-fluid .navbar-collapse .nav>li:not(.d-none){flex:1}.header-user-dropdown-toggle{text-align:center}.header-user-avatar{float:none}}.header-user.show .dropdown-menu{margin-top:4px;color:#303030;left:auto;max-height:445px}.header-user.show .dropdown-menu svg{vertical-align:text-top}.header-user-avatar{float:left;margin-right:5px;border-radius:50%;border:1px solid #f5f5f5}.media{display:flex;align-items:flex-start}.card{margin-bottom:16px}@media (min-width:768px){.page-with-contextual-sidebar{padding-left:50px}}@media (min-width:1200px){.page-with-contextual-sidebar{padding-left:220px}}.context-header{position:relative;margin-right:2px;width:220px}.context-header>a,.context-header>button{font-weight:600;display:flex;width:100%;align-items:center;padding:10px 16px 10px 10px;color:#303030;background-color:transparent;border:0;text-align:left}.context-header .avatar-container{flex:0 0 40px;background-color:#fff}.context-header .sidebar-context-title{overflow:hidden;text-overflow:ellipsis}.context-header .sidebar-context-title.text-secondary{font-weight:400;font-size:.8em}.nav-sidebar{position:fixed;z-index:600;width:220px;top:40px;bottom:0;left:0;background-color:#fafafa;box-shadow:inset -1px 0 0 #dbdbdb;transform:translate3d(0,0,0)}@media (min-width:576px) and (max-width:576px){.nav-sidebar:not(.sidebar-collapsed-desktop){box-shadow:inset -1px 0 0 #dbdbdb,2px 1px 3px rgba(0,0,0,.1)}}.nav-sidebar a{text-decoration:none}.nav-sidebar ul{padding-left:0;list-style:none}.nav-sidebar li{white-space:nowrap}.nav-sidebar li a{display:flex;align-items:center;padding:12px 16px;color:#707070}.nav-sidebar li .nav-item-name{flex:1}.nav-sidebar li.active>a,.sidebar-top-level-items>li.active .badge.badge-pill{font-weight:600}@media (max-width:767.98px){.nav-sidebar{left:-220px}}.nav-sidebar .nav-icon-container{display:flex;margin-right:8px}.nav-sidebar .fly-out-top-item{display:none}.nav-sidebar svg{height:16px;width:16px}@media (min-width:768px) and (max-width:1199px){.nav-sidebar:not(.sidebar-expanded-mobile){width:50px}.nav-sidebar:not(.sidebar-expanded-mobile) .nav-sidebar-inner-scroll{overflow-x:hidden}.nav-sidebar:not(.sidebar-expanded-mobile) .badge.badge-pill:not(.fly-out-badge),.nav-sidebar:not(.sidebar-expanded-mobile) .nav-item-name,.nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-context-title{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items>li>a{min-height:45px}.nav-sidebar:not(.sidebar-expanded-mobile) .fly-out-top-item{display:block}.nav-sidebar:not(.sidebar-expanded-mobile) .avatar-container{margin:0 auto}.nav-sidebar:not(.sidebar-expanded-mobile) .context-header{height:60px;width:50px}.nav-sidebar:not(.sidebar-expanded-mobile) .context-header a{padding:10px 4px}.nav-sidebar:not(.sidebar-expanded-mobile) .sidebar-top-level-items>li .sidebar-sub-level-items:not(.flyout-list),.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .collapse-text,.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-left{display:none}.nav-sidebar:not(.sidebar-expanded-mobile) .nav-icon-container{margin-right:0}.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button{padding:16px;width:49px}.nav-sidebar:not(.sidebar-expanded-mobile) .toggle-sidebar-button .icon-chevron-double-lg-right{display:block;margin:0}}.nav-sidebar-inner-scroll{height:100%;width:100%;overflow:auto}.sidebar-sub-level-items{display:none;padding-bottom:8px}.sidebar-sub-level-items>li a{padding:8px 16px 8px 40px}.sidebar-sub-level-items>li.active a,.sidebar-top-level-items>li.active{background:rgba(0,0,0,.04)}.sidebar-top-level-items{margin-bottom:60px}@media (min-width:576px){.sidebar-top-level-items>li>a{margin-right:1px}}.sidebar-top-level-items>li .badge.badge-pill{background-color:rgba(0,0,0,.08);color:#707070}.sidebar-top-level-items>li.active>a{margin-left:4px;padding-left:12px}.sidebar-top-level-items>li.active .sidebar-sub-level-items:not(.is-fly-out-only){display:block}.close-nav-button,.toggle-sidebar-button{width:219px;position:fixed;height:48px;bottom:0;padding:0 16px;background-color:#fafafa;border:0;border-top:1px solid #dbdbdb;color:#707070;display:flex;align-items:center}.close-nav-button svg,.toggle-sidebar-button svg{margin-right:8px}.close-nav-button .icon-chevron-double-lg-right,.toggle-sidebar-button .icon-chevron-double-lg-right{display:none}.collapse-text{white-space:nowrap;overflow:hidden}.fly-out-top-item>a{display:flex}.fly-out-top-item .fly-out-badge{margin-left:8px}.fly-out-top-item-name{flex:1}.close-nav-button{display:none}@media (max-width:767.98px){.close-nav-button{display:flex}.toggle-sidebar-button{display:none}}input::-moz-placeholder{color:#919191;opacity:1}input:-ms-input-placeholder,input::-ms-input-placeholder{color:#919191}svg.s12{width:12px;height:12px}svg.s16{width:16px;height:16px}svg.s18{width:18px;height:18px}.feature-highlight-popover-sub-content{padding:16px 12px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.color-label{padding:0 .5rem;line-height:16px;border-radius:100px;color:#fff}.label-link{display:inline-flex;vertical-align:text-bottom}.milestones{padding:8px;margin-top:8px;border-radius:4px;background-color:#dbdbdb}.search{margin:0 8px}@media (min-width:1200px){.search form{width:320px}}.search .search-input{border:0;font-size:14px;padding:0 20px 0 0;margin-left:5px;line-height:25px;width:98%;color:#fff;background:0 0}.search .search-input-container{display:flex;position:relative}.search .search-input-wrap{width:100%}.search .search-input-wrap .clear-icon,.search .search-input-wrap .search-icon{position:absolute;right:5px;top:4px}.search .search-input-wrap .search-icon{-moz-user-select:none;user-select:none}.search .search-input-wrap .clear-icon{display:none}.search .search-input-wrap .dropdown{position:static}.search .search-input-wrap .dropdown-menu{left:-5px;max-height:400px;overflow:auto}@media (min-width:1200px){.search .search-input-wrap .dropdown-menu{width:320px}}.search .search-input-wrap .dropdown-content{max-height:382px}.search .identicon{flex-basis:16px;flex-shrink:0;margin-right:4px}.settings{border-top:1px solid #dbdbdb}.settings:first-of-type{margin-top:10px;border:0}.settings+div .settings:first-of-type{margin-top:0;border-top:1px solid #dbdbdb}.avatar,.avatar-container{float:left;margin-right:16px;border-radius:50%;border:1px solid #f5f5f5}.s16.avatar,.s16.avatar-container{width:16px;height:16px;margin-right:8px}.s18.avatar,.s18.avatar-container{width:18px;height:18px;margin-right:8px}.s40.avatar,.s40.avatar-container{width:40px;height:40px;margin-right:8px}.avatar{transition-property:none;width:40px;height:40px;padding:0;background:#fdfdfd;overflow:hidden;border-color:rgba(0,0,0,.1)}.avatar.center{font-size:14px;line-height:1.8em;text-align:center}.avatar.avatar-tile{border-radius:0;border:0}.identicon{text-align:center;vertical-align:top;color:#4f4f4f;background-color:#eee}.identicon.s16{font-size:10px;line-height:16px}.identicon.s40{font-size:16px;line-height:38px}.avatar-container{overflow:hidden;display:flex}.avatar-container a{width:100%;height:100%;display:flex;text-decoration:none}.avatar-container .avatar{border-radius:0;border:0;height:auto;width:100%;margin:0;align-self:center}.avatar-container.s40{min-width:40px;min-height:40px}.rect-avatar,.rect-avatar.s16,.rect-avatar.s18{border-radius:2px}.rect-avatar.s40{border-radius:4px}.tab-width-8{-moz-tab-size:8;tab-size:8}.gl-sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.gl-ml-3{margin-left:.5rem}
+
+/* Cloaking in order to prevent flickering of content */
+@import 'cloaking';
+@include cloak-startup-scss(none);
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 38842ec167e..99a13cc4e44 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -14,12 +14,6 @@
#{'.text-#{$variant}-#{$suffix}'} {
color: $color;
}
-
- #{'.hover-text-#{$variant}-#{$suffix}'} {
- &:hover {
- color: $color;
- }
- }
}
}
@@ -82,6 +76,10 @@
.gl-h-32 { height: px-to-rem($grid-size * 4); }
.gl-h-64 { height: px-to-rem($grid-size * 8); }
+// Migrate this to Gitlab UI when FF is removed
+// https://gitlab.com/groups/gitlab-org/-/epics/2882
+.gl-h-200\! { height: px-to-rem($grid-size * 25) !important; }
+
.d-sm-table-column {
@include media-breakpoint-up(sm) {
display: table-column !important;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 41a6616d10c..3a5b8b2862e 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -16,7 +16,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
push_frontend_feature_flag(:ci_instance_variables_ui, default_enabled: true)
end
- VALID_SETTING_PANELS = %w(general integrations repository
+ VALID_SETTING_PANELS = %w(general repository
ci_cd reporting metrics_and_profiling
network preferences).freeze
@@ -32,12 +32,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def integrations
- if Feature.enabled?(:instance_level_integrations)
- @integrations = Service.find_or_initialize_instances.sort_by(&:title)
- else
- set_application_setting
- perform_update if submitted?
- end
+ @integrations = Service.find_or_initialize_instances.sort_by(&:title)
end
def update
@@ -225,7 +220,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:lets_encrypt_terms_of_service_accepted,
:domain_blacklist_file,
:raw_blob_request_limit,
- :namespace_storage_size_limit,
:issues_create_limit,
:default_branch_name,
disabled_oauth_sign_in_sources: [],
diff --git a/app/controllers/admin/integrations_controller.rb b/app/controllers/admin/integrations_controller.rb
index 4f3be43d14d..b2d5a2d130c 100644
--- a/app/controllers/admin/integrations_controller.rb
+++ b/app/controllers/admin/integrations_controller.rb
@@ -12,7 +12,7 @@ class Admin::IntegrationsController < Admin::ApplicationController
end
def integrations_enabled?
- Feature.enabled?(:instance_level_integrations)
+ true
end
def scoped_edit_integration_path(integration)
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index e0137accd2d..1bc82e98ab8 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -5,9 +5,6 @@ class Admin::ServicesController < Admin::ApplicationController
before_action :service, only: [:edit, :update]
before_action :whitelist_query_limiting, only: [:index]
- before_action only: :edit do
- push_frontend_feature_flag(:integration_form_refactor, default_enabled: true)
- end
def index
@services = Service.find_or_create_templates.sort_by(&:title)
diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb
index c79a0bb01bc..188805c6106 100644
--- a/app/controllers/clusters/base_controller.rb
+++ b/app/controllers/clusters/base_controller.rb
@@ -6,10 +6,6 @@ class Clusters::BaseController < ApplicationController
skip_before_action :authenticate_user!
before_action :authorize_read_cluster!
- before_action do
- push_frontend_feature_flag(:managed_apps_local_tiller, clusterable, default_enabled: true)
- end
-
helper_method :clusterable
private
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index b885e55f902..4b4bcc8d37e 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -33,9 +33,7 @@ module AuthenticatesWithTwoFactor
end
def locked_user_redirect(user)
- flash.now[:alert] = locked_user_redirect_alert(user)
-
- render 'devise/sessions/new'
+ redirect_to new_user_session_path, alert: locked_user_redirect_alert(user)
end
def authenticate_with_two_factor
@@ -54,7 +52,13 @@ module AuthenticatesWithTwoFactor
private
def locked_user_redirect_alert(user)
- user.access_locked? ? _('Your account is locked.') : _('Invalid Login or password')
+ if user.access_locked?
+ _('Your account is locked.')
+ elsif !user.confirmed?
+ I18n.t('devise.failure.unconfirmed')
+ else
+ _('Invalid Login or password')
+ end
end
def clear_two_factor_attempt!
diff --git a/app/controllers/concerns/checks_collaboration.rb b/app/controllers/concerns/checks_collaboration.rb
index 1fa82f7dcd4..87239facdeb 100644
--- a/app/controllers/concerns/checks_collaboration.rb
+++ b/app/controllers/concerns/checks_collaboration.rb
@@ -17,7 +17,7 @@ module ChecksCollaboration
# used across multiple calls in the view
def user_access(project)
@user_access ||= {}
- @user_access[project] ||= Gitlab::UserAccess.new(current_user, project: project)
+ @user_access[project] ||= Gitlab::UserAccess.new(current_user, container: project)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/graceful_timeout_handling.rb b/app/controllers/concerns/graceful_timeout_handling.rb
new file mode 100644
index 00000000000..490c0ec3b1d
--- /dev/null
+++ b/app/controllers/concerns/graceful_timeout_handling.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module GracefulTimeoutHandling
+ extend ActiveSupport::Concern
+
+ included do
+ rescue_from ActiveRecord::QueryCanceled do |exception|
+ raise exception unless request.format.json?
+
+ log_exception(exception)
+
+ render json: { error: _('There is too much data to calculate. Please change your selection.') }
+ end
+ end
+end
diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb
index 46febc44807..9a8e5d14123 100644
--- a/app/controllers/concerns/integrations_actions.rb
+++ b/app/controllers/concerns/integrations_actions.rb
@@ -8,9 +8,6 @@ module IntegrationsActions
before_action :not_found, unless: :integrations_enabled?
before_action :integration, only: [:edit, :update, :test]
- before_action only: :edit do
- push_frontend_feature_flag(:integration_form_refactor, default_enabled: true)
- end
end
def edit
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 4f61e5ed711..89ba2175b60 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -65,7 +65,7 @@ module IssuableCollections
def page_count_for_relation(relation, row_count)
limit = relation.limit_value.to_f
- return 1 if limit.zero?
+ return 1 if limit == 0
(row_count.to_f / limit).ceil
end
diff --git a/app/controllers/concerns/packages_access.rb b/app/controllers/concerns/packages_access.rb
new file mode 100644
index 00000000000..6df2e064bb2
--- /dev/null
+++ b/app/controllers/concerns/packages_access.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module PackagesAccess
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :verify_packages_enabled!
+ before_action :verify_read_package!
+ end
+
+ private
+
+ def verify_packages_enabled!
+ render_404 unless Gitlab.config.packages.enabled
+ end
+
+ def verify_read_package!
+ authorize_read_package!(project)
+ end
+end
diff --git a/app/controllers/concerns/paginated_collection.rb b/app/controllers/concerns/paginated_collection.rb
index be84215a9e2..fcee4493314 100644
--- a/app/controllers/concerns/paginated_collection.rb
+++ b/app/controllers/concerns/paginated_collection.rb
@@ -6,7 +6,7 @@ module PaginatedCollection
private
def redirect_out_of_range(collection, total_pages = collection.total_pages)
- return false if total_pages.zero?
+ return false if total_pages == 0
out_of_range = collection.current_page > total_pages
diff --git a/app/controllers/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb
index b8026c7a01d..a15bf27a22f 100644
--- a/app/controllers/concerns/renders_blob.rb
+++ b/app/controllers/concerns/renders_blob.rb
@@ -29,6 +29,12 @@ module RendersBlob
end
def conditionally_expand_blob(blob)
- blob.expand! if params[:expanded] == 'true'
+ conditionally_expand_blobs([blob])
+ end
+
+ def conditionally_expand_blobs(blobs)
+ return unless params[:expanded] == 'true'
+
+ blobs.each { |blob| blob.expand! }
end
end
diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb
index 2f5dc09be4a..7cb19fc7e58 100644
--- a/app/controllers/concerns/send_file_upload.rb
+++ b/app/controllers/concerns/send_file_upload.rb
@@ -18,7 +18,11 @@ module SendFileUpload
send_params.merge!(filename: attachment, disposition: disposition)
end
- if file_upload.file_storage?
+ if image_scaling_request?(file_upload)
+ location = file_upload.file_storage? ? file_upload.path : file_upload.url
+ headers.store(*Gitlab::Workhorse.send_scaled_image(location, params[:width].to_i))
+ head :ok
+ elsif file_upload.file_storage?
send_file file_upload.path, send_params
elsif file_upload.class.proxy_download_enabled? || proxy
headers.store(*Gitlab::Workhorse.send_url(file_upload.url(**redirect_params)))
@@ -37,4 +41,19 @@ module SendFileUpload
"application/octet-stream"
end
end
+
+ private
+
+ def image_scaling_request?(file_upload)
+ avatar_image_upload?(file_upload) && valid_image_scaling_width? && current_user &&
+ Feature.enabled?(:dynamic_image_resizing, current_user)
+ end
+
+ def avatar_image_upload?(file_upload)
+ file_upload.try(:image?) && file_upload.try(:mounted_as)&.to_sym == :avatar
+ end
+
+ def valid_image_scaling_width?
+ Avatarable::ALLOWED_IMAGE_SCALER_WIDTHS.include?(params[:width]&.to_i)
+ end
end
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index 048b18c5c61..5552fd663f7 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -55,10 +55,9 @@ module SnippetsActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def show
- conditionally_expand_blob(blob)
-
respond_to do |format|
format.html do
+ conditionally_expand_blob(blob)
@note = Note.new(noteable: @snippet, project: @snippet.project)
@noteable = @snippet
@@ -68,11 +67,14 @@ module SnippetsActions
end
format.json do
+ conditionally_expand_blob(blob)
render_blob_json(blob)
end
format.js do
if @snippet.embeddable?
+ conditionally_expand_blobs(blobs)
+
render 'shared/snippets/show'
else
head :not_found
@@ -109,13 +111,15 @@ module SnippetsActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def blob
- return unless snippet
+ @blob ||= blobs.first
+ end
- @blob ||= if snippet.empty_repo?
- snippet.blob
- else
- snippet.blobs.first
- end
+ def blobs
+ @blobs ||= if snippet.empty_repo?
+ [snippet.blob]
+ else
+ snippet.blobs
+ end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -132,6 +136,8 @@ module SnippetsActions
end
def redirect_if_binary
+ return if Feature.enabled?(:snippets_binary_blob)
+
redirect_to gitlab_snippet_path(snippet) if blob&.binary?
end
end
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index a5182000f5b..5b953fe37d6 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -8,6 +8,8 @@ module WikiActions
extend ActiveSupport::Concern
included do
+ before_action { respond_to :html }
+
before_action :authorize_read_wiki!
before_action :authorize_create_wiki!, only: [:edit, :create]
before_action :authorize_admin_wiki!, only: :destroy
@@ -65,6 +67,8 @@ module WikiActions
@ref = params[:version_id]
@path = page.path
+ Gitlab::UsageDataCounters::WikiPageCounter.count(:view)
+
render 'shared/wikis/show'
elsif file_blob
send_blob(wiki.repository, file_blob)
@@ -107,14 +111,16 @@ module WikiActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def create
- @page = WikiPages::CreateService.new(container: container, current_user: current_user, params: wiki_params).execute
+ response = WikiPages::CreateService.new(container: container, current_user: current_user, params: wiki_params).execute
+ @page = response.payload[:page]
- if page.persisted?
+ if response.success?
redirect_to(
wiki_page_path(wiki, page),
notice: _('Wiki was successfully updated.')
)
else
+ flash[:alert] = response.message
render 'shared/wikis/edit'
end
rescue Gitlab::Git::Wiki::OperationError => e
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index ad64b6c4f94..91704f030cd 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -9,7 +9,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
include FiltersEvents
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
- before_action :set_non_archived_param
+ before_action :set_non_archived_param, only: [:index, :starred]
before_action :set_sorting
before_action :projects, only: [:index]
skip_cross_project_access_check :index, :starred
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index db40b0bed77..4fc2f7b0571 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -9,8 +9,6 @@ class Dashboard::TodosController < Dashboard::ApplicationController
before_action :authorize_read_group!, only: :index
before_action :find_todos, only: [:index, :destroy_all]
- track_unique_visits :index, target_id: 'u_analytics_todos'
-
def index
@sort = params[:sort]
@todos = @todos.page(params[:page])
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index f1f41e67a4c..b3fa089a712 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -80,7 +80,7 @@ class Explore::ProjectsController < Explore::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def preload_associations(projects)
- projects.includes(:route, :creator, :group, namespace: [:route, :owner])
+ projects.includes(:route, :creator, :group, :project_feature, namespace: [:route, :owner])
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/groups/packages_controller.rb b/app/controllers/groups/packages_controller.rb
new file mode 100644
index 00000000000..600acc72e67
--- /dev/null
+++ b/app/controllers/groups/packages_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Groups
+ class PackagesController < Groups::ApplicationController
+ before_action :verify_packages_enabled!
+
+ private
+
+ def verify_packages_enabled!
+ render_404 unless group.packages_feature_enabled?
+ end
+ end
+end
diff --git a/app/controllers/groups/releases_controller.rb b/app/controllers/groups/releases_controller.rb
new file mode 100644
index 00000000000..500c57a6f3e
--- /dev/null
+++ b/app/controllers/groups/releases_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Groups
+ class ReleasesController < Groups::ApplicationController
+ def index
+ respond_to do |format|
+ format.json do
+ render json: ReleaseSerializer.new.represent(releases)
+ end
+ end
+ end
+
+ private
+
+ def releases
+ ReleasesFinder
+ .new(@group, current_user, { include_subgroups: true })
+ .execute(preload: false)
+ .page(params[:page])
+ .per(30)
+ end
+ end
+end
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index 02b015e8e53..fb639f6e472 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -15,7 +15,12 @@ module Groups
end
def update
- if @group.update(group_variables_params)
+ update_result = Ci::ChangeVariablesService.new(
+ container: @group, current_user: current_user,
+ params: group_variables_params
+ ).execute
+
+ if update_result
respond_to do |format|
format.json { render_group_variables }
end
diff --git a/app/controllers/import/available_namespaces_controller.rb b/app/controllers/import/available_namespaces_controller.rb
new file mode 100644
index 00000000000..7983b4f20b5
--- /dev/null
+++ b/app/controllers/import/available_namespaces_controller.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class Import::AvailableNamespacesController < ApplicationController
+ def index
+ render json: NamespaceSerializer.new.represent(current_user.manageable_groups_with_routes)
+ end
+end
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index bc05030f8af..8a7a4c92b37 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -41,6 +41,10 @@ class Import::BaseController < ApplicationController
raise NotImplementedError
end
+ def extra_representation_opts
+ {}
+ end
+
private
def filter_attribute
@@ -58,11 +62,11 @@ class Import::BaseController < ApplicationController
end
def serialized_provider_repos
- Import::ProviderRepoSerializer.new(current_user: current_user).represent(importable_repos, provider: provider_name, provider_url: provider_url)
+ Import::ProviderRepoSerializer.new(current_user: current_user).represent(importable_repos, provider: provider_name, provider_url: provider_url, **extra_representation_opts)
end
def serialized_incompatible_repos
- Import::ProviderRepoSerializer.new(current_user: current_user).represent(incompatible_repos, provider: provider_name, provider_url: provider_url)
+ Import::ProviderRepoSerializer.new(current_user: current_user).represent(incompatible_repos, provider: provider_name, provider_url: provider_url, **extra_representation_opts)
end
def serialized_imported_projects
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index efeff8439e4..4785a71b8a1 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -54,6 +54,16 @@ class Import::GiteaController < Import::GithubController
end
end
+ override :client_repos
+ def client_repos
+ @client_repos ||= filtered(client.repos)
+ end
+
+ override :client
+ def client
+ @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
+ end
+
override :client_options
def client_options
{ host: provider_url, api_version: 'v1' }
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index ac6b8c06d66..29fe34f0734 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -10,6 +10,9 @@ class Import::GithubController < Import::BaseController
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
@@ -22,7 +25,7 @@ class Import::GithubController < Import::BaseController
end
def callback
- session[access_token_key] = client.get_token(params[:code])
+ session[access_token_key] = get_token(params[:code])
redirect_to status_import_url
end
@@ -77,9 +80,7 @@ class Import::GithubController < Import::BaseController
override :provider_url
def provider_url
strong_memoize(:provider_url) do
- provider = Gitlab::Auth::OAuth::Provider.config_for('github')
-
- provider&.dig('url').presence || 'https://github.com'
+ oauth_config&.dig('url').presence || 'https://github.com'
end
end
@@ -104,11 +105,66 @@ class Import::GithubController < Import::BaseController
end
def client
- @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
+ @client ||= if Feature.enabled?(:remove_legacy_github_client)
+ Gitlab::GithubImport::Client.new(session[access_token_key])
+ else
+ Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options)
+ end
end
def client_repos
- @client_repos ||= filtered(client.repos)
+ @client_repos ||= if Feature.enabled?(:remove_legacy_github_client)
+ filtered(concatenated_repos)
+ else
+ filtered(client.repos)
+ end
+ end
+
+ def concatenated_repos
+ return [] unless client.respond_to?(:each_page)
+
+ client.each_page(:repos).flat_map(&:objects)
+ 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
+ if Feature.enabled?(:remove_legacy_github_client)
+ oauth_client.auth_code.authorize_url(
+ redirect_uri: callback_import_url,
+ scope: 'repo, user, user:email'
+ )
+ else
+ client.authorize_url(callback_import_url)
+ 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
@@ -116,7 +172,7 @@ class Import::GithubController < Import::BaseController
end
def go_to_provider_for_permissions
- redirect_to client.authorize_url(callback_import_url)
+ redirect_to authorize_url
end
def import_enabled?
@@ -152,6 +208,12 @@ 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 access_token_key
:"#{provider_name}_access_token"
end
diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb
index 9aec870c6ea..9c47e6d4b0b 100644
--- a/app/controllers/import/manifest_controller.rb
+++ b/app/controllers/import/manifest_controller.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class Import::ManifestController < Import::BaseController
+ extend ::Gitlab::Utils::Override
+
before_action :whitelist_query_limiting, only: [:create]
before_action :verify_import_enabled
before_action :ensure_import_vars, only: [:create, :status]
@@ -8,16 +10,9 @@ class Import::ManifestController < Import::BaseController
def new
end
- # rubocop: disable CodeReuse/ActiveRecord
def status
- @already_added_projects = find_already_added_projects
- already_added_import_urls = @already_added_projects.pluck(:import_url)
-
- @pending_repositories = repositories.to_a.reject do |repository|
- already_added_import_urls.include?(repository[:url])
- end
+ super
end
- # rubocop: enable CodeReuse/ActiveRecord
def upload
group = Group.find(params[:group_id])
@@ -42,8 +37,8 @@ class Import::ManifestController < Import::BaseController
end
end
- def jobs
- render json: find_jobs
+ def realtime_changes
+ super
end
def create
@@ -54,12 +49,43 @@ class Import::ManifestController < Import::BaseController
project = Gitlab::ManifestImport::ProjectCreator.new(repository, group, current_user).execute
if project.persisted?
- render json: ProjectSerializer.new.represent(project)
+ render json: ProjectSerializer.new.represent(project, serializer: :import)
else
render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
end
+ protected
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ override :importable_repos
+ def importable_repos
+ already_added_projects_names = already_added_projects.pluck(:import_url)
+
+ repositories.reject { |repo| already_added_projects_names.include?(repo[:url]) }
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ override :incompatible_repos
+ def incompatible_repos
+ []
+ end
+
+ override :provider_name
+ def provider_name
+ :manifest
+ end
+
+ override :provider_url
+ def provider_url
+ nil
+ end
+
+ override :extra_representation_opts
+ def extra_representation_opts
+ { group_full_path: group.full_path }
+ end
+
private
def ensure_import_vars
@@ -82,15 +108,6 @@ class Import::ManifestController < Import::BaseController
find_already_added_projects.to_json(only: [:id], methods: [:import_status])
end
- # rubocop: disable CodeReuse/ActiveRecord
- def find_already_added_projects
- group.all_projects
- .where(import_type: 'manifest')
- .where(creator_id: current_user)
- .with_import_state
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def verify_import_enabled
render_404 unless manifest_import_enabled?
end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 5bd9ac7f275..29cafbbbdb6 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -4,6 +4,7 @@ class InvitesController < ApplicationController
include Gitlab::Utils::StrongMemoize
before_action :member
+ before_action :invite_details
skip_before_action :authenticate_user!, only: :decline
helper_method :member?, :current_user_matches_invite?
@@ -16,9 +17,8 @@ class InvitesController < ApplicationController
def accept
if member.accept_invite!(current_user)
- label, path = source_info(member.source)
-
- redirect_to path, notice: _("You have been granted %{member_human_access} access to %{label}.") % { member_human_access: member.human_access, label: label }
+ redirect_to invite_details[:path], notice: _("You have been granted %{member_human_access} access to %{title} %{name}.") %
+ { member_human_access: member.human_access, title: invite_details[:title], name: invite_details[:name] }
else
redirect_back_or_default(options: { alert: _("The invitation could not be accepted.") })
end
@@ -26,8 +26,6 @@ class InvitesController < ApplicationController
def decline
if member.decline_invite!
- label, _ = source_info(member.source)
-
path =
if current_user
dashboard_projects_path
@@ -35,7 +33,8 @@ class InvitesController < ApplicationController
new_user_session_path
end
- redirect_to path, notice: _("You have declined the invitation to join %{label}.") % { label: label }
+ redirect_to path, notice: _("You have declined the invitation to join %{title} %{name}.") %
+ { title: invite_details[:title], name: invite_details[:name] }
else
redirect_back_or_default(options: { alert: _("The invitation could not be declined.") })
end
@@ -76,24 +75,25 @@ class InvitesController < ApplicationController
notice = notice.join(' ') + "."
store_location_for :user, request.fullpath
- redirect_to new_user_session_path, notice: notice
+ redirect_to new_user_session_path(invite_email: member.invite_email), notice: notice
end
- def source_info(source)
- case source
- when Project
- project = member.source
- label = "project #{project.full_name}"
- path = project_path(project)
- when Group
- group = member.source
- label = "group #{group.name}"
- path = group_path(group)
- else
- label = "who knows what"
- path = dashboard_projects_path
- end
-
- [label, path]
+ def invite_details
+ @invite_details ||= case @member.source
+ when Project
+ {
+ name: @member.source.full_name,
+ url: project_url(@member.source),
+ title: _("project"),
+ path: project_path(@member.source)
+ }
+ when Group
+ {
+ name: @member.source.name,
+ url: group_url(@member.source),
+ title: _("group"),
+ path: group_path(@member.source)
+ }
+ end
end
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 706a4843117..6a393405e4d 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -27,6 +27,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
user = User.by_login(params[:username])
user&.increment_failed_attempts!
+ log_failed_login(params[:username], failed_strategy.name)
end
super
@@ -90,6 +91,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
private
+ def log_failed_login(user, provider)
+ # overridden in EE
+ end
+
def after_omniauth_failure_path_for(scope)
if Feature.enabled?(:user_mode_in_session)
return new_admin_session_path if current_user_mode.admin_mode_requested?
@@ -198,6 +203,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def fail_login(user)
+ log_failed_login(user.username, oauth['provider'])
+
error_message = user.errors.full_messages.to_sentence
redirect_to omniauth_error_path(oauth['provider'], error: error_message)
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index d2787c2e450..fccbc29f598 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -52,7 +52,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute
if result[:status] == :success
- flash[:notice] = _('Password was successfully updated. Please login with it')
+ flash[:notice] = _('Password was successfully updated. Please sign in again.')
redirect_to new_user_session_path
else
@user.reset
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 30f25e8fdaa..21adc032940 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -20,12 +20,8 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def revoke
@personal_access_token = finder.find(params[:id])
-
- if @personal_access_token.revoke!
- flash[:notice] = _("Revoked personal access token %{personal_access_token_name}!") % { personal_access_token_name: @personal_access_token.name }
- else
- flash[:alert] = _("Could not revoke personal access token %{personal_access_token_name}.") % { personal_access_token_name: @personal_access_token.name }
- end
+ service = PersonalAccessTokens::RevokeService.new(current_user, token: @personal_access_token).execute
+ service.success? ? flash[:notice] = service.message : flash[:alert] = service.message
redirect_to profile_personal_access_tokens_path
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index fef3c6cf424..652687932fd 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -108,7 +108,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def validate_artifacts!
- render_404 unless build&.artifacts?
+ render_404 unless build&.available_artifacts?
end
def build
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 7f14522e61b..d969e7bf771 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -257,5 +257,3 @@ class Projects::BlobController < Projects::ApplicationController
params.permit(:full, :since, :to, :bottom, :unfold, :offset, :indent)
end
end
-
-Projects::BlobController.prepend_if_ee('EE::Projects::BlobController')
diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb
index 73b3eb9c205..c13baaea8c6 100644
--- a/app/controllers/projects/ci/lints_controller.rb
+++ b/app/controllers/projects/ci/lints_controller.rb
@@ -8,16 +8,30 @@ class Projects::Ci::LintsController < Projects::ApplicationController
def create
@content = params[:content]
- result = Gitlab::Ci::YamlProcessor.new_with_validation_errors(@content, yaml_processor_options)
-
- @status = result.valid?
- @errors = result.errors
-
- if result.valid?
- @config_processor = result.config
- @stages = @config_processor.stages
- @builds = @config_processor.builds
- @jobs = @config_processor.jobs
+ @dry_run = params[:dry_run]
+
+ if @dry_run && Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project)
+ pipeline = Ci::CreatePipelineService
+ .new(@project, current_user, ref: @project.default_branch)
+ .execute(:push, dry_run: true, content: @content)
+
+ @status = pipeline.error_messages.empty?
+ @stages = pipeline.stages
+ @errors = pipeline.error_messages.map(&:content)
+ @warnings = pipeline.warning_messages.map(&:content)
+ else
+ result = Gitlab::Ci::YamlProcessor.new_with_validation_errors(@content, yaml_processor_options)
+
+ @status = result.valid?
+ @errors = result.errors
+ @warnings = result.warnings
+
+ if result.valid?
+ @config_processor = result.config
+ @stages = @config_processor.stages
+ @builds = @config_processor.builds
+ @jobs = @config_processor.jobs
+ end
end
render :show
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 3f2dc9b09fa..b0c6f3cc6a1 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -15,8 +15,8 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_download_code!
before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :commit
- before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines, :merge_requests]
- before_action :define_note_vars, only: [:show, :diff_for_path]
+ before_action :define_commit_vars, only: [:show, :diff_for_path, :diff_files, :pipelines, :merge_requests]
+ before_action :define_note_vars, only: [:show, :diff_for_path, :diff_files]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
BRANCH_SEARCH_LIMIT = 1000
@@ -41,6 +41,10 @@ class Projects::CommitController < Projects::ApplicationController
render_diff_for_path(@commit.diffs(diff_options))
end
+ def diff_files
+ render json: { html: view_to_html_string('projects/commit/diff_files', diffs: @diffs, environment: @environment) }
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def pipelines
@pipelines = @commit.pipelines.order(id: :desc)
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
index 673f53c221b..c69bf029c73 100644
--- a/app/controllers/projects/cycle_analytics/events_controller.rb
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -4,6 +4,7 @@ module Projects
module CycleAnalytics
class EventsController < Projects::ApplicationController
include CycleAnalyticsParams
+ include GracefulTimeoutHandling
before_action :authorize_read_cycle_analytics!
before_action :authorize_read_build!, only: [:test, :staging]
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index 898d888c978..ef97bc795f9 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -5,6 +5,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::TextHelper
include CycleAnalyticsParams
include Analytics::UniqueVisitsHelper
+ include GracefulTimeoutHandling
before_action :whitelist_query_limiting, only: [:show]
before_action :authorize_read_cycle_analytics!
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index d5da24a76de..71195fdb892 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -13,6 +13,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
authorize_metrics_dashboard!
push_frontend_feature_flag(:prometheus_computed_alerts)
+ push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
end
before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect]
before_action :authorize_create_environment!, only: [:new, :create]
@@ -104,7 +105,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
action_or_env_url =
if stop_action
- polymorphic_url([project.namespace.becomes(Namespace), project, stop_action])
+ polymorphic_url([project, stop_action])
else
project_environment_url(project, @environment)
end
@@ -158,18 +159,14 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def metrics_redirect
- environment = project.default_environment
-
- if environment
- redirect_to environment_metrics_path(environment)
- else
- render :empty_metrics
- end
+ redirect_to project_metrics_dashboard_path(project)
end
def metrics
respond_to do |format|
- format.html
+ format.html do
+ redirect_to project_metrics_dashboard_path(project, environment: environment )
+ end
format.json do
# Currently, this acts as a hint to load the metrics details into the cache
# if they aren't there already
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index b93f6384e0c..41631aea620 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -36,7 +36,19 @@ class Projects::ForksController < Projects::ApplicationController
end
def new
- @namespaces = fork_service.valid_fork_targets - [project.namespace]
+ respond_to do |format|
+ format.html do
+ @own_namespace = current_user.namespace if fork_service.valid_fork_targets.include?(current_user.namespace)
+ @project = project
+ end
+
+ format.json do
+ namespaces = fork_service.valid_fork_targets - [current_user.namespace, project.namespace]
+ render json: {
+ namespaces: ForkNamespaceSerializer.new.represent(namespaces, project: project, current_user: current_user)
+ }
+ end
+ end
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
new file mode 100644
index 00000000000..12cc4dde1f4
--- /dev/null
+++ b/app/controllers/projects/incidents_controller.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class Projects::IncidentsController < Projects::ApplicationController
+ before_action :authorize_read_incidents!
+
+ def index
+ end
+end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 12b5a538bc9..2200860a184 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -51,8 +51,10 @@ class Projects::IssuesController < Projects::ApplicationController
end
before_action only: :show do
- push_frontend_feature_flag(:real_time_issue_sidebar, @project)
- push_frontend_feature_flag(:confidential_apollo_sidebar, @project)
+ real_time_feature_flag = :real_time_issue_sidebar
+ real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project)
+
+ gon.push({ features: { real_time_feature_flag.to_s.camelize(:lower) => real_time_enabled } }, true)
end
before_action only: :index do
@@ -88,7 +90,7 @@ class Projects::IssuesController < Projects::ApplicationController
params[:issue] ||= ActionController::Parameters.new(
assignee_ids: ""
)
- build_params = issue_params.merge(
+ build_params = issue_create_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve],
confidential: !!Gitlab::Utils.to_boolean(params[:issue][:confidential])
@@ -108,7 +110,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create
- create_params = issue_params.merge(spammable_params).merge(
+ create_params = issue_create_params.merge(spammable_params).merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve]
)
@@ -291,6 +293,16 @@ class Projects::IssuesController < Projects::ApplicationController
] + [{ label_ids: [], assignee_ids: [], update_task: [:index, :checked, :line_number, :line_source] }]
end
+ def issue_create_params
+ create_params = %i[
+ issue_type
+ ]
+
+ params.require(:issue).permit(
+ *create_params
+ ).merge(issue_params)
+ end
+
def reorder_params
params.permit(:move_before_id, :move_after_id, :group_full_path)
end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 98b0abc89e9..bceccc7063b 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -41,7 +41,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def diffs_metadata
diffs = @compare.diffs(diff_options)
- render json: DiffsMetadataSerializer.new(project: @merge_request.project)
+ render json: DiffsMetadataSerializer.new(project: @merge_request.project, current_user: current_user)
.represent(diffs, additional_attributes)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 5d4514be838..e77d2f0f5ee 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -31,16 +31,19 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
push_frontend_feature_flag(:code_navigation, @project, default_enabled: true)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
- push_frontend_feature_flag(:merge_ref_head_comments, @project)
+ push_frontend_feature_flag(:merge_ref_head_comments, @project, default_enabled: true)
push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true)
- push_frontend_feature_flag(:multiline_comments, @project)
+ push_frontend_feature_flag(:multiline_comments, @project, default_enabled: true)
push_frontend_feature_flag(:file_identifier_hash)
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
+ push_frontend_feature_flag(:auto_expand_collapsed_diffs, @project, default_enabled: true)
+ push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
+ push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
+ push_frontend_feature_flag(:merge_request_widget_graphql, @project)
end
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, @project.group)
- push_frontend_feature_flag(:junit_pipeline_view, @project.group)
end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
@@ -80,7 +83,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@note = @project.notes.new(noteable: @merge_request)
@noteable = @merge_request
- @commits_count = @merge_request.commits_count
+ @commits_count = @merge_request.commits_count + @merge_request.context_commits_count
@issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
@current_user_data = UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json
@show_whitespace_default = current_user.nil? || current_user.show_whitespace_in_diffs
@@ -114,6 +117,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def commits
+ # Get context commits from repository
+ @context_commits =
+ set_commits_for_rendering(
+ @merge_request.recent_context_commits
+ )
+
# Get commits from repository
# or from cache if already merged
@commits =
@@ -403,7 +412,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
return access_denied! unless @merge_request.source_branch_exists?
access_check = ::Gitlab::UserAccess
- .new(current_user, project: @merge_request.source_project)
+ .new(current_user, container: @merge_request.source_project)
.can_push_to_branch?(@merge_request.source_branch)
access_denied! unless access_check
diff --git a/app/controllers/projects/metrics/dashboards/builder_controller.rb b/app/controllers/projects/metrics/dashboards/builder_controller.rb
new file mode 100644
index 00000000000..2ab574d7d10
--- /dev/null
+++ b/app/controllers/projects/metrics/dashboards/builder_controller.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Projects
+ module Metrics
+ module Dashboards
+ class BuilderController < Projects::ApplicationController
+ before_action :authorize_metrics_dashboard!
+
+ def panel_preview
+ respond_to do |format|
+ format.json do
+ if rendered_panel.success?
+ render json: rendered_panel.payload
+ else
+ render json: { message: rendered_panel.message }, status: :unprocessable_entity
+ end
+ end
+ end
+ end
+
+ private
+
+ def rendered_panel
+ @panel_preview ||= ::Metrics::Dashboard::PanelPreviewService.new(project, panel_yaml, environment).execute
+ end
+
+ def panel_yaml
+ params.require(:panel_yaml)
+ end
+
+ def environment
+ @environment ||=
+ if params[:environment]
+ project.environments.find(params[:environment])
+ else
+ project.default_environment
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/metrics_dashboard_controller.rb b/app/controllers/projects/metrics_dashboard_controller.rb
index 235ee1dfbf2..51307c3665c 100644
--- a/app/controllers/projects/metrics_dashboard_controller.rb
+++ b/app/controllers/projects/metrics_dashboard_controller.rb
@@ -9,13 +9,14 @@ module Projects
before_action :authorize_metrics_dashboard!
before_action do
push_frontend_feature_flag(:prometheus_computed_alerts)
+ push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
end
def show
if environment
render 'projects/environments/metrics'
else
- render_404
+ render 'projects/environments/empty_metrics'
end
end
diff --git a/app/controllers/projects/packages/package_files_controller.rb b/app/controllers/projects/packages/package_files_controller.rb
new file mode 100644
index 00000000000..dd6d875cd1e
--- /dev/null
+++ b/app/controllers/projects/packages/package_files_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Projects
+ module Packages
+ class PackageFilesController < ApplicationController
+ include PackagesAccess
+ include SendFileUpload
+
+ def download
+ package_file = project.package_files.find(params[:id])
+
+ send_upload(package_file.file, attachment: package_file.file_name)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/packages/packages_controller.rb b/app/controllers/projects/packages/packages_controller.rb
new file mode 100644
index 00000000000..fc4ef7a01dc
--- /dev/null
+++ b/app/controllers/projects/packages/packages_controller.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Projects
+ module Packages
+ class PackagesController < Projects::ApplicationController
+ include PackagesAccess
+
+ before_action :authorize_destroy_package!, only: [:destroy]
+
+ def show
+ @package = project.packages.find(params[:id])
+ @package_files = @package.package_files.recent
+ @maven_metadatum = @package.maven_metadatum
+ end
+
+ def destroy
+ @package = project.packages.find(params[:id])
+ @package.destroy
+
+ redirect_to project_packages_path(@project), status: :found, notice: _('Package was removed')
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/pipelines/tests_controller.rb b/app/controllers/projects/pipelines/tests_controller.rb
index f03274bf32e..1c212964df5 100644
--- a/app/controllers/projects/pipelines/tests_controller.rb
+++ b/app/controllers/projects/pipelines/tests_controller.rb
@@ -3,7 +3,6 @@
module Projects
module Pipelines
class TestsController < Projects::Pipelines::ApplicationController
- before_action :validate_feature_flag!
before_action :authorize_read_build!
before_action :builds, only: [:show]
@@ -29,29 +28,21 @@ module Projects
private
- def validate_feature_flag!
- render_404 unless Feature.enabled?(:build_report_summary, project)
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def builds
- pipeline.latest_builds.where(id: build_params)
+ @builds ||= pipeline.latest_builds.for_ids(build_ids).presence || render_404
end
- def build_params
+ def build_ids
return [] unless params[:build_ids]
params[:build_ids].split(",")
end
def test_suite
- if builds.present?
- builds.map do |build|
- build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
- end.sum
- else
- render_404
- end
+ builds.map do |build|
+ build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
+ end.sum
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index d8e11ddd423..bfe23eb1035 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -12,11 +12,10 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
- push_frontend_feature_flag(:junit_pipeline_view, project)
- push_frontend_feature_flag(:build_report_summary, project)
push_frontend_feature_flag(:filter_pipelines_search, project, default_enabled: true)
push_frontend_feature_flag(:dag_pipeline_tab, project, default_enabled: true)
push_frontend_feature_flag(:pipelines_security_report_summary, project)
+ push_frontend_feature_flag(:new_pipeline_form)
end
before_action :ensure_pipeline, only: [:show]
@@ -177,8 +176,6 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def test_report
- return unless Feature.enabled?(:junit_pipeline_view, project)
-
respond_to do |format|
format.html do
render 'show'
@@ -192,12 +189,6 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
- def test_reports_count
- return unless Feature.enabled?(:junit_pipeline_view, project)
-
- render json: { total_count: pipeline.test_reports_count }.to_json
- end
-
private
def serialize_pipelines
diff --git a/app/controllers/projects/product_analytics_controller.rb b/app/controllers/projects/product_analytics_controller.rb
new file mode 100644
index 00000000000..badd7671dcf
--- /dev/null
+++ b/app/controllers/projects/product_analytics_controller.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+class Projects::ProductAnalyticsController < Projects::ApplicationController
+ before_action :feature_enabled!
+ before_action :authorize_read_product_analytics!
+ before_action :tracker_variables, only: [:setup, :test]
+
+ def index
+ @events = product_analytics_events.order_by_time.page(params[:page])
+ end
+
+ def setup
+ end
+
+ def test
+ @event = product_analytics_events.try(:first)
+ end
+
+ def graphs
+ @graphs = []
+ @timerange = 30
+
+ requested_graphs = %w(platform os_timezone br_lang doc_charset)
+
+ requested_graphs.each do |graph|
+ @graphs << ProductAnalytics::BuildGraphService
+ .new(project, { graph: graph, timerange: @timerange })
+ .execute
+ end
+ end
+
+ private
+
+ def product_analytics_events
+ @project.product_analytics_events
+ end
+
+ def tracker_variables
+ # We use project id as Snowplow appId
+ @project_id = @project.id.to_s
+
+ # Snowplow remembers values like appId and platform between reloads.
+ # That is why we have to rename the tracker with a random integer.
+ @random = rand(999999)
+
+ # Generate random platform every time a tracker is rendered.
+ @platform = %w(web mob app)[(@random % 3)]
+ end
+
+ def feature_enabled!
+ render_404 unless Feature.enabled?(:product_analytics, @project, default_enabled: false)
+ end
+end
diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb
index 2c0521edece..c6ae65f7832 100644
--- a/app/controllers/projects/prometheus/alerts_controller.rb
+++ b/app/controllers/projects/prometheus/alerts_controller.rb
@@ -39,7 +39,7 @@ module Projects
render json: serialize_as_json(@alert)
else
- head :no_content
+ head :bad_request
end
end
@@ -49,7 +49,7 @@ module Projects
render json: serialize_as_json(alert)
else
- head :no_content
+ head :bad_request
end
end
@@ -59,14 +59,14 @@ module Projects
head :ok
else
- head :no_content
+ head :bad_request
end
end
private
def alerts_params
- params.permit(:operator, :threshold, :environment_id, :prometheus_metric_id)
+ params.permit(:operator, :threshold, :environment_id, :prometheus_metric_id, :runbook_url)
end
def notify_service
diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb
index d9921757502..060403a9cd9 100644
--- a/app/controllers/projects/protected_refs_controller.rb
+++ b/app/controllers/projects/protected_refs_controller.rb
@@ -62,7 +62,7 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
end
def access_level_attributes
- %i[access_level id]
+ %i[access_level id _destroy]
end
end
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index d58755c2655..c48d573edbf 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -29,7 +29,7 @@ class Projects::ReleasesController < Projects::ApplicationController
end
def new
- unless Feature.enabled?(:new_release_page, project)
+ unless Feature.enabled?(:new_release_page, project, default_enabled: true)
redirect_to(new_project_tag_path(@project))
end
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 6b7e253595c..ca2a19e67b0 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -12,7 +12,6 @@ class Projects::ServicesController < Projects::ApplicationController
before_action :set_deprecation_notice_for_prometheus_service, only: [:edit, :update]
before_action :redirect_deprecated_prometheus_service, only: [:update]
before_action only: :edit do
- push_frontend_feature_flag(:integration_form_refactor, default_enabled: true)
push_frontend_feature_flag(:jira_issues_integration, @project, { default_enabled: true })
end
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index d7a6f1b0139..781b850ddfe 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -6,10 +6,6 @@ module Projects
before_action :authorize_admin_operations!
before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token]
- before_action do
- push_frontend_feature_flag(:pagerduty_webhook, project)
- end
-
respond_to :json, only: [:reset_alerting_token, :reset_pagerduty_token]
helper_method :error_tracking_setting
@@ -49,7 +45,7 @@ module Projects
if result[:status] == :success
pagerduty_token = project.incident_management_setting&.pagerduty_token
- webhook_url = project_incidents_pagerduty_url(project, token: pagerduty_token)
+ webhook_url = project_incidents_integrations_pagerduty_url(project, token: pagerduty_token)
render json: { pagerduty_webhook_url: webhook_url, pagerduty_token: pagerduty_token }
else
diff --git a/app/controllers/projects/snippets/blobs_controller.rb b/app/controllers/projects/snippets/blobs_controller.rb
index 148fc7c96f8..eaec8600d77 100644
--- a/app/controllers/projects/snippets/blobs_controller.rb
+++ b/app/controllers/projects/snippets/blobs_controller.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class Projects::Snippets::BlobsController < Projects::Snippets::ApplicationController
- include Snippets::BlobsActions
+ include ::Snippets::BlobsActions
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 49840e847f2..632e8db9796 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -14,6 +14,10 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
before_action :authorize_update_snippet!, only: [:edit, :update]
before_action :authorize_admin_snippet!, only: [:destroy]
+ before_action do
+ push_frontend_feature_flag(:snippet_multiple_files, current_user)
+ end
+
def index
@snippet_counts = ::Snippets::CountService
.new(current_user, project: @project)
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 2cc030d18fc..0fd047f90cf 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -12,7 +12,12 @@ class Projects::VariablesController < Projects::ApplicationController
end
def update
- if @project.update(variables_params)
+ update_result = Ci::ChangeVariablesService.new(
+ container: @project, current_user: current_user,
+ params: variables_params
+ ).execute
+
+ if update_result
respond_to do |format|
format.json { render_variables }
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a5666cb70ac..ba21fbddde1 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -38,9 +38,16 @@ class ProjectsController < Projects::ApplicationController
before_action only: [:new, :create] do
frontend_experimentation_tracking_data(:new_create_project_ui, 'click_tab')
push_frontend_feature_flag(:new_create_project_ui) if experiment_enabled?(:new_create_project_ui)
+ end
+
+ before_action only: [:edit] do
push_frontend_feature_flag(:service_desk_custom_address, @project)
end
+ before_action only: [:edit] do
+ push_frontend_feature_flag(:approval_suggestions, @project)
+ end
+
layout :determine_layout
def index
@@ -392,6 +399,7 @@ class ProjectsController < Projects::ApplicationController
:initialize_with_readme,
:autoclose_referenced_issues,
:suggestion_commit_message,
+ :packages_enabled,
:service_desk_enabled,
project_feature_attributes: %i[
diff --git a/app/controllers/registrations/experience_levels_controller.rb b/app/controllers/registrations/experience_levels_controller.rb
index 97239b1bbac..5bb039bd9ba 100644
--- a/app/controllers/registrations/experience_levels_controller.rb
+++ b/app/controllers/registrations/experience_levels_controller.rb
@@ -2,9 +2,7 @@
module Registrations
class ExperienceLevelsController < ApplicationController
- # This will need to be changed to simply 'devise' as part of
- # https://gitlab.com/gitlab-org/growth/engineering/issues/64
- layout 'devise_experimental_separate_sign_up_flow'
+ layout 'devise_experimental_onboarding_issues'
before_action :check_experiment_enabled
before_action :ensure_namespace_path_param
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index b1c1fe3ba74..2a865aac767 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -17,7 +17,8 @@ class RegistrationsController < Devise::RegistrationsController
def new
if experiment_enabled?(:signup_flow)
- track_experiment_event(:signup_flow, 'start') # We want this event to be tracked when the user is _in_ the experimental group
+ track_experiment_event(:terms_opt_in, 'start')
+
@resource = build_resource
else
redirect_to new_user_session_path(anchor: 'register-pane')
@@ -25,8 +26,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def create
- track_experiment_event(:signup_flow, 'end') unless experiment_enabled?(:signup_flow) # We want this event to be tracked when the user is _in_ the control group
-
+ track_experiment_event(:terms_opt_in, 'end')
accept_pending_invitations
super do |new_user|
@@ -62,9 +62,11 @@ class RegistrationsController < Devise::RegistrationsController
result = ::Users::SignupService.new(current_user, user_params).execute
if result[:status] == :success
- track_experiment_event(:signup_flow, 'end') # We want this event to be tracked when the user is _in_ the experimental group
+ if ::Gitlab.com? && show_onboarding_issues_experiment?
+ track_experiment_event(:onboarding_issues, 'signed_up')
+ record_experiment_user(:onboarding_issues)
+ end
- track_experiment_event(:onboarding_issues, 'signed_up') if ::Gitlab.com? && show_onboarding_issues_experiment?
return redirect_to new_users_sign_up_group_path if experiment_enabled?(:onboarding_issues) && show_onboarding_issues_experiment?
set_flash_message! :notice, :signed_up
@@ -178,6 +180,8 @@ class RegistrationsController < Devise::RegistrationsController
end
def terms_accepted?
+ return true if experiment_enabled?(:terms_opt_in)
+
Gitlab::Utils.to_boolean(params[:terms_opt_in])
end
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index 6a27d63625e..aa6609bef2a 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -105,7 +105,7 @@ module Repositories
access.check(git_command, Gitlab::GitAccess::ANY)
if repo_type.project? && !container
- @project = @container = access.project
+ @project = @container = access.container
end
end
diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb
index ec5ca5bbeec..0436b740979 100644
--- a/app/controllers/repositories/lfs_storage_controller.rb
+++ b/app/controllers/repositories/lfs_storage_controller.rb
@@ -73,9 +73,8 @@ module Repositories
# rubocop: enable CodeReuse/ActiveRecord
def create_file!(oid, size)
- uploaded_file = UploadedFile.from_params(
- params, :file, LfsObjectUploader.workhorse_local_upload_path)
- return unless uploaded_file
+ uploaded_file = params[:file]
+ return unless uploaded_file.is_a?(UploadedFile)
LfsObject.create!(oid: oid, size: size, file: uploaded_file)
end
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 14469877e14..191134472c2 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -13,6 +13,7 @@ class RootController < Dashboard::ProjectsController
before_action :redirect_unlogged_user, if: -> { current_user.nil? }
before_action :redirect_logged_user, if: -> { current_user.present? }
+ before_action :customize_homepage, only: :index, if: -> { current_user.present? }
# We only need to load the projects when the user is logged in but did not
# configure a dashboard. In which case we render projects. We can do that straight
# from the #index action.
@@ -66,6 +67,10 @@ class RootController < Dashboard::ProjectsController
root_urls.exclude?(home_page_url)
end
+
+ def customize_homepage
+ @customize_homepage = experiment_enabled?(:customize_homepage)
+ end
end
RootController.prepend_if_ee('EE::RootController')
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index ff6d9350a5c..56b6a5201e7 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -6,7 +6,8 @@ class SearchController < ApplicationController
include RendersCommits
SCOPE_PRELOAD_METHOD = {
- projects: :with_web_entity_associations
+ projects: :with_web_entity_associations,
+ issues: :with_web_entity_associations
}.freeze
around_action :allow_gitaly_ref_name_caching
@@ -113,4 +114,15 @@ class SearchController < ApplicationController
Gitlab::UsageDataCounters::SearchCounter.count(:navbar_searches)
end
+
+ def append_info_to_payload(payload)
+ super
+
+ # Merging to :metadata will ensure these are logged as top level keys
+ payload[:metadata] || {}
+ payload[:metadata]['meta.search.group_id'] = params[:group_id]
+ payload[:metadata]['meta.search.project_id'] = params[:project_id]
+ payload[:metadata]['meta.search.search'] = params[:search]
+ payload[:metadata]['meta.search.scope'] = params[:scope]
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 9e8075d4bcc..f82212591b6 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -25,7 +25,7 @@ class SessionsController < Devise::SessionsController
before_action :store_unauthenticated_sessions, only: [:new]
before_action :save_failed_login, if: :action_new_and_failed_login?
before_action :load_recaptcha
- before_action :frontend_tracking_data, only: [:new]
+ before_action :set_invite_params, only: [:new]
after_action :log_failed_login, if: :action_new_and_failed_login?
after_action :verify_known_sign_in, only: [:create]
@@ -293,9 +293,8 @@ class SessionsController < Devise::SessionsController
end
end
- def frontend_tracking_data
- # We want tracking data pushed to the frontend when the user is _in_ the control group
- frontend_experimentation_tracking_data(:signup_flow, 'start') unless experiment_enabled?(:signup_flow)
+ def set_invite_params
+ @invite_email = ActionController::Base.helpers.sanitize(params[:invite_email])
end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index e68b821459d..486c7f1d028 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -17,6 +17,10 @@ class SnippetsController < Snippets::ApplicationController
layout 'snippets'
+ before_action do
+ push_frontend_feature_flag(:snippet_multiple_files, current_user)
+ end
+
def index
if params[:username].present?
@user = UserFinder.new(params[:username]).find_by_username!
diff --git a/app/finders/ci/daily_build_group_report_results_finder.rb b/app/finders/ci/daily_build_group_report_results_finder.rb
index 774f08d1ff2..ec41d9d2c45 100644
--- a/app/finders/ci/daily_build_group_report_results_finder.rb
+++ b/app/finders/ci/daily_build_group_report_results_finder.rb
@@ -14,17 +14,25 @@ module Ci
end
def execute
- return none unless can?(current_user, :read_build_report_results, project)
+ return none unless query_allowed?
+ query
+ end
+
+ protected
+
+ attr_reader :current_user, :project, :ref_path, :start_date, :end_date, :limit
+
+ def query
Ci::DailyBuildGroupReportResult.recent_results(
query_params,
limit: limit
)
end
- private
-
- attr_reader :current_user, :project, :ref_path, :start_date, :end_date, :limit
+ def query_allowed?
+ can?(current_user, :read_build_report_results, project)
+ end
def query_params
{
diff --git a/app/finders/concerns/merged_at_filter.rb b/app/finders/concerns/merged_at_filter.rb
new file mode 100644
index 00000000000..d2858ba2f88
--- /dev/null
+++ b/app/finders/concerns/merged_at_filter.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module MergedAtFilter
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def by_merged_at(items)
+ return items unless merged_after || merged_before
+
+ mr_metrics_scope = MergeRequest::Metrics
+ mr_metrics_scope = mr_metrics_scope.merged_after(merged_after) if merged_after.present?
+ mr_metrics_scope = mr_metrics_scope.merged_before(merged_before) if merged_before.present?
+
+ scope = items.joins(:metrics).merge(mr_metrics_scope)
+ scope = target_project_id_filter_on_metrics(scope) if Feature.enabled?(:improved_mr_merged_at_queries)
+ scope
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def merged_after
+ params[:merged_after]
+ end
+
+ def merged_before
+ params[:merged_before]
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def target_project_id_filter_on_metrics(scope)
+ scope.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/app/finders/context_commits_finder.rb b/app/finders/context_commits_finder.rb
index f1b3eb43e84..de89a556ee0 100644
--- a/app/finders/context_commits_finder.rb
+++ b/app/finders/context_commits_finder.rb
@@ -25,7 +25,7 @@ class ContextCommitsFinder
if search.present?
search_commits
else
- project.repository.commits(merge_request.source_branch, { limit: limit, offset: offset })
+ project.repository.commits(merge_request.target_branch, { limit: limit, offset: offset })
end
commits
@@ -47,7 +47,7 @@ class ContextCommitsFinder
commits = [commit_by_sha] if commit_by_sha
end
else
- commits = project.repository.find_commits_by_message(search, nil, nil, 20)
+ commits = project.repository.find_commits_by_message(search, merge_request.target_branch, nil, 20)
end
commits
diff --git a/app/finders/design_management/designs_finder.rb b/app/finders/design_management/designs_finder.rb
index 10f95520d1e..d9732f6b6f4 100644
--- a/app/finders/design_management/designs_finder.rb
+++ b/app/finders/design_management/designs_finder.rb
@@ -22,7 +22,9 @@ module DesignManagement
items = by_filename(items)
items = by_id(items)
- items
+ # TODO: We don't need to pass the project anymore after the feature flag is removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/34382
+ items.ordered(issue.project)
end
private
@@ -35,7 +37,8 @@ module DesignManagement
issue.designs
end
- # Returns all designs that existed at a particular design version
+ # Returns all designs that existed at a particular design version,
+ # where `nil` means `at-current-version`.
def by_visible_at_version(items)
items.visible_at_version(params[:visible_at_version])
end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 949af103eb3..a4b00588368 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -57,7 +57,7 @@ class GroupMembersFinder < UnionFinder
members = members.search(params[:search]) if params[:search].present?
members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
- if can_manage_members && params[:two_factor].present?
+ if params[:two_factor].present? && can_manage_members
members = members.filter_by_2fa(params[:two_factor])
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 2b2e6b377b4..bbb624f543b 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -25,6 +25,7 @@
# updated_after: datetime
# updated_before: datetime
# confidential: boolean
+# issue_type: array of strings (one of Issue.issue_types)
#
class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
@@ -73,6 +74,7 @@ class IssuesFinder < IssuableFinder
issues = super
issues = by_due_date(issues)
issues = by_confidential(issues)
+ issues = by_issue_types(issues)
issues
end
@@ -97,6 +99,14 @@ class IssuesFinder < IssuableFinder
items.due_between(Date.today - 2.weeks, (Date.today + 1.month).end_of_month)
end
end
+
+ def by_issue_types(items)
+ issue_type_params = Array(params[:issue_types]).map(&:to_s)
+ return items if issue_type_params.blank?
+ return Issue.none unless (Issue.issue_types.keys & issue_type_params).sort == issue_type_params.sort
+
+ items.with_issue_type(params[:issue_types])
+ end
end
IssuesFinder.prepend_if_ee('EE::IssuesFinder')
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index e08ed737ca6..ce9137f91bb 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -29,7 +29,12 @@ class MembersFinder
def find_members(include_relations)
project_members = project.project_members
- project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
+
+ if params[:active_without_invites_and_requests].present?
+ project_members = project_members.active_without_invites_and_requests
+ else
+ project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
+ end
return project_members if include_relations == [:direct]
@@ -44,6 +49,7 @@ class MembersFinder
def filter_members(members)
members = members.search(params[:search]) if params[:search].present?
members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
+ members = members.owners_and_maintainers if params[:owners_and_maintainers].present?
members
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index d5e6c4783c1..b70d0b7a06a 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -30,8 +30,10 @@
# updated_before: datetime
#
class MergeRequestsFinder < IssuableFinder
+ include MergedAtFilter
+
def self.scalar_params
- @scalar_params ||= super + [:wip, :target_branch]
+ @scalar_params ||= super + [:wip, :draft, :target_branch, :merged_after, :merged_before]
end
def klass
@@ -42,8 +44,9 @@ class MergeRequestsFinder < IssuableFinder
items = by_commit(super)
items = by_deployment(items)
items = by_source_branch(items)
- items = by_wip(items)
+ items = by_draft(items)
items = by_target_branch(items)
+ items = by_merged_at(items)
by_source_project_id(items)
end
@@ -88,20 +91,32 @@ class MergeRequestsFinder < IssuableFinder
items.where(source_project_id: source_project_id)
end
- def by_wip(items)
- if params[:wip] == 'yes'
+ def by_draft(items)
+ draft_param = params[:draft] || params[:wip]
+
+ if draft_param == 'yes'
items.where(wip_match(items.arel_table))
- elsif params[:wip] == 'no'
+ elsif draft_param == 'no'
items.where.not(wip_match(items.arel_table))
else
items
end
end
+ # WIP is deprecated in favor of Draft. Currently both options are supported
def wip_match(table)
- table[:title].matches('WIP:%')
+ items =
+ table[:title].matches('WIP:%')
.or(table[:title].matches('WIP %'))
.or(table[:title].matches('[WIP]%'))
+
+ return items unless Feature.enabled?(:merge_request_draft_filter)
+
+ items
+ .or(table[:title].matches('Draft - %'))
+ .or(table[:title].matches('Draft:%'))
+ .or(table[:title].matches('[Draft]%'))
+ .or(table[:title].matches('(Draft)%'))
end
def by_deployment(items)
diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb
index 8f0cdf3b255..16e59b31b36 100644
--- a/app/finders/milestones_finder.rb
+++ b/app/finders/milestones_finder.rb
@@ -3,6 +3,7 @@
# Search for milestones
#
# params - Hash
+# ids - filters by id.
# project_ids: Array of project ids or single project id or ActiveRecord relation.
# group_ids: Array of group ids or single group id or ActiveRecord relation.
# order - Orders by field default due date asc.
@@ -21,6 +22,7 @@ class MilestonesFinder
def execute
items = Milestone.all
+ items = by_ids(items)
items = by_groups_and_projects(items)
items = by_title(items)
items = by_search_title(items)
@@ -32,6 +34,12 @@ class MilestonesFinder
private
+ def by_ids(items)
+ return items unless params[:ids].present?
+
+ items.id_in(params[:ids])
+ end
+
def by_groups_and_projects(items)
items.for_projects_and_groups(params[:project_ids], params[:group_ids])
end
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index e3d5f2ae8de..93f8c520b63 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -5,12 +5,14 @@ class PersonalAccessTokensFinder
delegate :build, :find, :find_by_id, :find_by_token, to: :execute
- def initialize(params = {})
+ def initialize(params = {}, current_user = nil)
@params = params
+ @current_user = current_user
end
def execute
tokens = PersonalAccessToken.all
+ tokens = by_current_user(tokens)
tokens = by_user(tokens)
tokens = by_impersonation(tokens)
tokens = by_state(tokens)
@@ -20,6 +22,15 @@ class PersonalAccessTokensFinder
private
+ attr_reader :current_user
+
+ def by_current_user(tokens)
+ return tokens if current_user.nil? || current_user.admin?
+ return PersonalAccessToken.none unless Ability.allowed?(current_user, :read_user_personal_access_tokens, params[:user])
+
+ tokens
+ end
+
def by_user(tokens)
return tokens unless @params[:user]
diff --git a/app/finders/releases_finder.rb b/app/finders/releases_finder.rb
index 6a754fdb5a1..e961ad4c0ca 100644
--- a/app/finders/releases_finder.rb
+++ b/app/finders/releases_finder.rb
@@ -1,19 +1,20 @@
# frozen_string_literal: true
class ReleasesFinder
- attr_reader :project, :current_user, :params
+ include Gitlab::Utils::StrongMemoize
- def initialize(project, current_user = nil, params = {})
- @project = project
+ attr_reader :parent, :current_user, :params
+
+ def initialize(parent, current_user = nil, params = {})
+ @parent = parent
@current_user = current_user
@params = params
end
def execute(preload: true)
- return Release.none unless Ability.allowed?(current_user, :read_release, project)
+ return Release.none if projects.empty?
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/211988
- releases = project.releases.where.not(tag: nil) # rubocop:disable CodeReuse/ActiveRecord
+ releases = get_releases
releases = by_tag(releases)
releases = releases.preloaded if preload
releases.sorted
@@ -21,6 +22,34 @@ class ReleasesFinder
private
+ def get_releases
+ Release.where(project_id: projects).where.not(tag: nil) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def include_subgroups?
+ params.fetch(:include_subgroups, false)
+ end
+
+ def projects
+ strong_memoize(:projects) do
+ if parent.is_a?(Project)
+ Ability.allowed?(current_user, :read_release, parent) ? [parent] : []
+ elsif parent.is_a?(Group)
+ accessible_projects
+ end
+ end
+ end
+
+ def accessible_projects
+ projects = if include_subgroups?
+ Project.for_group_and_its_subgroups(parent)
+ else
+ parent.projects
+ end
+
+ projects.select { |project| Ability.allowed?(current_user, :read_release, project) }
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def by_tag(releases)
return releases unless params[:tag].present?
diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb
index 78c8392f1cd..a35376e905e 100644
--- a/app/finders/template_finder.rb
+++ b/app/finders/template_finder.rb
@@ -6,7 +6,10 @@ class TemplateFinder
VENDORED_TEMPLATES = HashWithIndifferentAccess.new(
dockerfiles: ::Gitlab::Template::DockerfileTemplate,
gitignores: ::Gitlab::Template::GitignoreTemplate,
- gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate
+ gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate,
+ metrics_dashboard_ymls: ::Gitlab::Template::MetricsDashboardTemplate,
+ issues: ::Gitlab::Template::IssueTemplate,
+ merge_requests: ::Gitlab::Template::MergeRequestTemplate
).freeze
class << self
@@ -34,9 +37,9 @@ class TemplateFinder
def execute
if params[:name]
- vendored_templates.find(params[:name])
+ vendored_templates.find(params[:name], project)
else
- vendored_templates.all
+ vendored_templates.all(project)
end
end
end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index a2054f73c9d..f28e1281488 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -10,6 +10,7 @@
# action_id: integer
# author_id: integer
# project_id; integer
+# target_id; integer
# state: 'pending' (default) or 'done'
# type: 'Issue' or 'MergeRequest' or ['Issue', 'MergeRequest']
#
@@ -23,7 +24,7 @@ class TodosFinder
NONE = '0'
- TODO_TYPES = Set.new(%w(Issue MergeRequest DesignManagement::Design)).freeze
+ TODO_TYPES = Set.new(%w(Issue MergeRequest DesignManagement::Design AlertManagement::Alert)).freeze
attr_accessor :current_user, :params
@@ -47,6 +48,7 @@ class TodosFinder
items = by_action(items)
items = by_author(items)
items = by_state(items)
+ items = by_target_id(items)
items = by_types(items)
items = by_group(items)
# Filtering by project HAS TO be the last because we use
@@ -198,6 +200,12 @@ class TodosFinder
items.with_states(params[:state])
end
+ def by_target_id(items)
+ return items if params[:target_id].blank?
+
+ items.for_target(params[:target_id])
+ end
+
def by_types(items)
if types.any?
items.for_type(types)
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 592167a633b..d8967da9f57 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -48,6 +48,13 @@ class GitlabSchema < GraphQL::Schema
super(query_str, **kwargs)
end
+ def get_type(type_name)
+ # This is a backwards compatibility hack to work around an accidentally
+ # released argument typed as EEIterationID
+ type_name = type_name.gsub(/^EE/, '') if type_name.end_with?('ID')
+ super(type_name)
+ end
+
def id_from_object(object, _type = nil, _ctx = nil)
unless object.respond_to?(:to_global_id)
# This is an error in our schema and needs to be solved. So raise a
@@ -77,6 +84,8 @@ class GitlabSchema < GraphQL::Schema
# will be called.
# * All other classes will use `GlobalID#find`
def find_by_gid(gid)
+ return unless gid
+
if gid.model_class < ApplicationRecord
Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find
elsif gid.model_class.respond_to?(:lazy_find)
@@ -142,6 +151,13 @@ class GitlabSchema < GraphQL::Schema
end
end
end
+
+ # This is a backwards compatibility hack to work around an accidentally
+ # released argument typed as EE{Type}ID
+ def get_type(type_name)
+ type_name = type_name.gsub(/^EE/, '') if type_name.end_with?('ID')
+ super(type_name)
+ end
end
GitlabSchema.prepend_if_ee('EE::GitlabSchema') # rubocop: disable Cop/InjectEnterpriseEditionModule
diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb
new file mode 100644
index 00000000000..d4bf47af4cf
--- /dev/null
+++ b/app/graphql/mutations/boards/issues/issue_move_list.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Boards
+ module Issues
+ class IssueMoveList < Mutations::Issues::Base
+ graphql_name 'IssueMoveList'
+
+ argument :board_id, GraphQL::ID_TYPE,
+ required: true,
+ loads: Types::BoardType,
+ description: 'Global ID of the board that the issue is in'
+
+ argument :project_path, GraphQL::ID_TYPE,
+ required: true,
+ description: 'Project the issue to mutate is in'
+
+ argument :iid, GraphQL::STRING_TYPE,
+ required: true,
+ description: 'IID of the issue to mutate'
+
+ argument :from_list_id, GraphQL::ID_TYPE,
+ required: false,
+ description: 'ID of the board list that the issue will be moved from'
+
+ argument :to_list_id, GraphQL::ID_TYPE,
+ required: false,
+ description: 'ID of the board list that the issue will be moved to'
+
+ argument :move_before_id, GraphQL::ID_TYPE,
+ required: false,
+ description: 'ID of issue before which the current issue will be positioned at'
+
+ argument :move_after_id, GraphQL::ID_TYPE,
+ required: false,
+ description: 'ID of issue after which the current issue will be positioned at'
+
+ def ready?(**args)
+ if move_arguments(args).blank?
+ raise Gitlab::Graphql::Errors::ArgumentError,
+ 'At least one of the arguments fromListId, toListId, afterId or beforeId is required'
+ end
+
+ if move_list_arguments(args).one?
+ raise Gitlab::Graphql::Errors::ArgumentError,
+ 'Both fromListId and toListId must be present'
+ end
+
+ super
+ end
+
+ def resolve(board:, **args)
+ raise_resource_not_available_error! unless board
+ authorize_board!(board)
+
+ issue = authorized_find!(project_path: args[:project_path], iid: args[:iid])
+ move_params = { id: issue.id, board_id: board.id }.merge(move_arguments(args))
+
+ move_issue(board, issue, move_params)
+
+ {
+ issue: issue.reset,
+ errors: issue.errors.full_messages
+ }
+ end
+
+ private
+
+ def move_issue(board, issue, move_params)
+ service = ::Boards::Issues::MoveService.new(board.resource_parent, current_user, move_params)
+
+ service.execute(issue)
+ end
+
+ def move_list_arguments(args)
+ args.slice(:from_list_id, :to_list_id)
+ end
+
+ def move_arguments(args)
+ args.slice(:from_list_id, :to_list_id, :move_after_id, :move_before_id)
+ end
+
+ def authorize_board!(board)
+ return if Ability.allowed?(current_user, :read_board, board.resource_parent)
+
+ raise_resource_not_available_error!
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/boards/lists/base.rb b/app/graphql/mutations/boards/lists/base.rb
new file mode 100644
index 00000000000..34b271ba3b8
--- /dev/null
+++ b/app/graphql/mutations/boards/lists/base.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Boards
+ module Lists
+ class Base < BaseMutation
+ include Mutations::ResolvesIssuable
+
+ argument :board_id, ::Types::GlobalIDType[::Board],
+ required: true,
+ description: 'The Global ID of the issue board to mutate'
+
+ field :list,
+ Types::BoardListType,
+ null: true,
+ description: 'List of the issue board'
+
+ authorize :admin_list
+
+ private
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::Board)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/boards/lists/create.rb b/app/graphql/mutations/boards/lists/create.rb
new file mode 100644
index 00000000000..4f545709ee9
--- /dev/null
+++ b/app/graphql/mutations/boards/lists/create.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Boards
+ module Lists
+ class Create < Base
+ graphql_name 'BoardListCreate'
+
+ argument :backlog, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'Create the backlog list'
+
+ argument :label_id, ::Types::GlobalIDType[::Label],
+ required: false,
+ description: 'ID of an existing label'
+
+ def ready?(**args)
+ if args.slice(*mutually_exclusive_args).size != 1
+ arg_str = mutually_exclusive_args.map { |x| x.to_s.camelize(:lower) }.join(' or ')
+ raise Gitlab::Graphql::Errors::ArgumentError, "one and only one of #{arg_str} is required"
+ end
+
+ super
+ end
+
+ def resolve(**args)
+ board = authorized_find!(id: args[:board_id])
+ params = create_list_params(args)
+
+ authorize_list_type_resource!(board, params)
+
+ list = create_list(board, params)
+
+ {
+ list: list.valid? ? list : nil,
+ errors: errors_on_object(list)
+ }
+ end
+
+ private
+
+ def authorize_list_type_resource!(board, params)
+ return unless params[:label_id]
+
+ labels = ::Labels::AvailableLabelsService.new(current_user, board.resource_parent, params)
+ .filter_labels_ids_in_param(:label_id)
+
+ unless labels.present?
+ raise Gitlab::Graphql::Errors::ArgumentError, 'Label not found!'
+ end
+ end
+
+ def create_list(board, params)
+ create_list_service =
+ ::Boards::Lists::CreateService.new(board.resource_parent, current_user, params)
+
+ create_list_service.execute(board)
+ end
+
+ def create_list_params(args)
+ params = args.slice(*mutually_exclusive_args).with_indifferent_access
+ params[:label_id] = GitlabSchema.parse_gid(params[:label_id]).model_id if params[:label_id]
+
+ params
+ end
+
+ def mutually_exclusive_args
+ [:backlog, :label_id]
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/boards/lists/update.rb b/app/graphql/mutations/boards/lists/update.rb
new file mode 100644
index 00000000000..7efed3058b3
--- /dev/null
+++ b/app/graphql/mutations/boards/lists/update.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Boards
+ module Lists
+ class Update < BaseMutation
+ graphql_name 'UpdateBoardList'
+
+ argument :list_id, GraphQL::ID_TYPE,
+ required: true,
+ loads: Types::BoardListType,
+ description: 'Global ID of the list.'
+
+ argument :position, GraphQL::INT_TYPE,
+ required: false,
+ description: 'Position of list within the board'
+
+ argument :collapsed, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'Indicates if list is collapsed for this user'
+
+ field :list,
+ Types::BoardListType,
+ null: true,
+ description: 'Mutated list'
+
+ def resolve(list: nil, **args)
+ raise_resource_not_available_error! unless can_read_list?(list)
+ update_result = update_list(list, args)
+
+ {
+ list: update_result[:list],
+ errors: list.errors.full_messages
+ }
+ end
+
+ private
+
+ def update_list(list, args)
+ service = ::Boards::Lists::UpdateService.new(list.board, current_user, args)
+ service.execute(list)
+ end
+
+ def can_read_list?(list)
+ return false unless list.present?
+
+ Ability.allowed?(current_user, :read_list, list.board)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/concerns/mutations/assignable.rb b/app/graphql/mutations/concerns/mutations/assignable.rb
new file mode 100644
index 00000000000..f6f4b744f4e
--- /dev/null
+++ b/app/graphql/mutations/concerns/mutations/assignable.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Assignable
+ extend ActiveSupport::Concern
+
+ included do
+ argument :assignee_usernames,
+ [GraphQL::STRING_TYPE],
+ required: true,
+ description: 'The usernames to assign to the resource. Replaces existing assignees by default.'
+
+ argument :operation_mode,
+ Types::MutationOperationModeEnum,
+ required: false,
+ description: 'The operation to perform. Defaults to REPLACE.'
+ end
+
+ def resolve(project_path:, iid:, assignee_usernames:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
+ resource = authorized_find!(project_path: project_path, iid: iid)
+
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/36098') if resource.is_a?(MergeRequest)
+
+ update_service_class.new(
+ resource.project,
+ current_user,
+ assignee_ids: assignee_ids(resource, assignee_usernames, operation_mode)
+ ).execute(resource)
+
+ {
+ resource.class.name.underscore.to_sym => resource,
+ errors: errors_on_object(resource)
+ }
+ end
+
+ private
+
+ def assignee_ids(resource, usernames, operation_mode)
+ assignee_ids = []
+ assignee_ids += resource.assignees.map(&:id) if Types::MutationOperationModeEnum.enum.values_at(:remove, :append).include?(operation_mode)
+ user_ids = UsersFinder.new(current_user, username: usernames).execute.map(&:id)
+
+ if operation_mode == Types::MutationOperationModeEnum.enum[:remove]
+ assignee_ids -= user_ids
+ else
+ assignee_ids |= user_ids
+ end
+
+ assignee_ids
+ end
+ end
+end
diff --git a/app/graphql/mutations/concerns/mutations/resolves_subscription.rb b/app/graphql/mutations/concerns/mutations/resolves_subscription.rb
new file mode 100644
index 00000000000..e8c5d0d404d
--- /dev/null
+++ b/app/graphql/mutations/concerns/mutations/resolves_subscription.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Mutations
+ module ResolvesSubscription
+ extend ActiveSupport::Concern
+ included do
+ argument :subscribed_state,
+ GraphQL::BOOLEAN_TYPE,
+ required: true,
+ description: 'The desired state of the subscription'
+ end
+
+ def resolve(project_path:, iid:, subscribed_state:)
+ resource = authorized_find!(project_path: project_path, iid: iid)
+ project = resource.project
+
+ resource.set_subscription(current_user, subscribed_state, project)
+
+ {
+ resource.class.name.underscore.to_sym => resource,
+ errors: errors_on_object(resource)
+ }
+ end
+ end
+end
diff --git a/app/graphql/mutations/design_management/move.rb b/app/graphql/mutations/design_management/move.rb
new file mode 100644
index 00000000000..0b654447844
--- /dev/null
+++ b/app/graphql/mutations/design_management/move.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Mutations
+ module DesignManagement
+ class Move < ::Mutations::BaseMutation
+ graphql_name "DesignManagementMove"
+
+ DesignID = ::Types::GlobalIDType[::DesignManagement::Design]
+
+ argument :id, DesignID, required: true, as: :current_design,
+ description: "ID of the design to move"
+
+ argument :previous, DesignID, required: false, as: :previous_design,
+ description: "ID of the immediately preceding design"
+
+ argument :next, DesignID, required: false, as: :next_design,
+ description: "ID of the immediately following design"
+
+ field :design_collection, Types::DesignManagement::DesignCollectionType,
+ null: true,
+ description: "The current state of the collection"
+
+ def ready(*)
+ raise ::Gitlab::Graphql::Errors::ResourceNotAvailable unless ::Feature.enabled?(:reorder_designs, default_enabled: true)
+ end
+
+ def resolve(**args)
+ service = ::DesignManagement::MoveDesignsService.new(current_user, parameters(args))
+
+ { design_collection: service.collection, errors: service.execute.errors }
+ end
+
+ private
+
+ def parameters(**args)
+ args.transform_values { |id| GitlabSchema.find_by_gid(id) }.transform_values(&:sync).tap do |hash|
+ hash.each { |k, design| not_found(args[k]) unless current_user.can?(:read_design, design) }
+ end
+ end
+
+ def not_found(gid)
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, "Resource not available: #{gid}"
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/issues/base.rb b/app/graphql/mutations/issues/base.rb
index 7c545c3eb00..529d48f3cd0 100644
--- a/app/graphql/mutations/issues/base.rb
+++ b/app/graphql/mutations/issues/base.rb
@@ -11,7 +11,7 @@ module Mutations
argument :iid, GraphQL::STRING_TYPE,
required: true,
- description: "The iid of the issue to mutate"
+ description: "The IID of the issue to mutate"
field :issue,
Types::IssueType,
diff --git a/app/graphql/mutations/issues/set_assignees.rb b/app/graphql/mutations/issues/set_assignees.rb
new file mode 100644
index 00000000000..a4d1c755b53
--- /dev/null
+++ b/app/graphql/mutations/issues/set_assignees.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Issues
+ class SetAssignees < Base
+ graphql_name 'IssueSetAssignees'
+
+ include Assignable
+
+ def update_service_class
+ ::Issues::UpdateService
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/issues/set_subscription.rb b/app/graphql/mutations/issues/set_subscription.rb
new file mode 100644
index 00000000000..a04c8f5ba2d
--- /dev/null
+++ b/app/graphql/mutations/issues/set_subscription.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Issues
+ class SetSubscription < Base
+ graphql_name 'IssueSetSubscription'
+
+ include ResolvesSubscription
+ end
+ end
+end
diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb
index 7f6d9b0f988..cc03d32731b 100644
--- a/app/graphql/mutations/issues/update.rb
+++ b/app/graphql/mutations/issues/update.rb
@@ -25,6 +25,27 @@ module Mutations
required: false,
description: copy_field_description(Types::IssueType, :confidential)
+ argument :locked,
+ GraphQL::BOOLEAN_TYPE,
+ as: :discussion_locked,
+ required: false,
+ description: copy_field_description(Types::IssueType, :discussion_locked)
+
+ argument :add_label_ids,
+ [GraphQL::ID_TYPE],
+ required: false,
+ description: 'The IDs of labels to be added to the issue.'
+
+ argument :remove_label_ids,
+ [GraphQL::ID_TYPE],
+ required: false,
+ description: 'The IDs of labels to be removed from the issue.'
+
+ argument :milestone_id,
+ GraphQL::ID_TYPE,
+ required: false,
+ description: 'The ID of the milestone to be assigned, milestone will be removed if set to null.'
+
def resolve(project_path:, iid:, **args)
issue = authorized_find!(project_path: project_path, iid: iid)
project = issue.project
diff --git a/app/graphql/mutations/merge_requests/create.rb b/app/graphql/mutations/merge_requests/create.rb
index e210987f259..fd2cd58a5ee 100644
--- a/app/graphql/mutations/merge_requests/create.rb
+++ b/app/graphql/mutations/merge_requests/create.rb
@@ -27,6 +27,10 @@ module Mutations
required: false,
description: copy_field_description(Types::MergeRequestType, :description)
+ argument :labels, [GraphQL::STRING_TYPE],
+ required: false,
+ description: copy_field_description(Types::MergeRequestType, :labels)
+
field :merge_request,
Types::MergeRequestType,
null: true,
@@ -34,18 +38,11 @@ module Mutations
authorize :create_merge_request_from
- def resolve(project_path:, title:, source_branch:, target_branch:, description: nil)
+ def resolve(project_path:, **attributes)
project = authorized_find!(full_path: project_path)
+ params = attributes.merge(author_id: current_user.id)
- attributes = {
- title: title,
- source_branch: source_branch,
- target_branch: target_branch,
- author_id: current_user.id,
- description: description
- }
-
- merge_request = ::MergeRequests::CreateService.new(project, current_user, attributes).execute
+ merge_request = ::MergeRequests::CreateService.new(project, current_user, params).execute
{
merge_request: merge_request.valid? ? merge_request : nil,
diff --git a/app/graphql/mutations/merge_requests/set_assignees.rb b/app/graphql/mutations/merge_requests/set_assignees.rb
index de244b62d0f..548c6b55a85 100644
--- a/app/graphql/mutations/merge_requests/set_assignees.rb
+++ b/app/graphql/mutations/merge_requests/set_assignees.rb
@@ -5,43 +5,10 @@ module Mutations
class SetAssignees < Base
graphql_name 'MergeRequestSetAssignees'
- argument :assignee_usernames,
- [GraphQL::STRING_TYPE],
- required: true,
- description: <<~DESC
- The usernames to assign to the merge request. Replaces existing assignees by default.
- DESC
+ include Assignable
- argument :operation_mode,
- Types::MutationOperationModeEnum,
- required: false,
- description: <<~DESC
- The operation to perform. Defaults to REPLACE.
- DESC
-
- def resolve(project_path:, iid:, assignee_usernames:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/36098')
-
- merge_request = authorized_find!(project_path: project_path, iid: iid)
- project = merge_request.project
-
- assignee_ids = []
- assignee_ids += merge_request.assignees.map(&:id) if Types::MutationOperationModeEnum.enum.values_at(:remove, :append).include?(operation_mode)
- user_ids = UsersFinder.new(current_user, username: assignee_usernames).execute.map(&:id)
-
- if operation_mode == Types::MutationOperationModeEnum.enum[:remove]
- assignee_ids -= user_ids
- else
- assignee_ids |= user_ids
- end
-
- ::MergeRequests::UpdateService.new(project, current_user, assignee_ids: assignee_ids)
- .execute(merge_request)
-
- {
- merge_request: merge_request,
- errors: errors_on_object(merge_request)
- }
+ def update_service_class
+ ::MergeRequests::UpdateService
end
end
end
diff --git a/app/graphql/mutations/merge_requests/set_subscription.rb b/app/graphql/mutations/merge_requests/set_subscription.rb
index 1535481ab37..7d3c40185c9 100644
--- a/app/graphql/mutations/merge_requests/set_subscription.rb
+++ b/app/graphql/mutations/merge_requests/set_subscription.rb
@@ -5,22 +5,7 @@ module Mutations
class SetSubscription < Base
graphql_name 'MergeRequestSetSubscription'
- argument :subscribed_state,
- GraphQL::BOOLEAN_TYPE,
- required: true,
- description: 'The desired state of the subscription'
-
- def resolve(project_path:, iid:, subscribed_state:)
- merge_request = authorized_find!(project_path: project_path, iid: iid)
- project = merge_request.project
-
- merge_request.set_subscription(current_user, subscribed_state, project)
-
- {
- merge_request: merge_request,
- errors: errors_on_object(merge_request)
- }
- end
+ include ResolvesSubscription
end
end
end
diff --git a/app/graphql/mutations/notes/update/base.rb b/app/graphql/mutations/notes/update/base.rb
index 9a53337f253..8a2a78a29ec 100644
--- a/app/graphql/mutations/notes/update/base.rb
+++ b/app/graphql/mutations/notes/update/base.rb
@@ -40,7 +40,7 @@ module Mutations
end
def note_params(_note, args)
- { note: args[:body] }.compact
+ { note: args[:body], confidential: args[:confidential] }.compact
end
end
end
diff --git a/app/graphql/mutations/notes/update/note.rb b/app/graphql/mutations/notes/update/note.rb
index 03a174fc8d9..ca97dad6ded 100644
--- a/app/graphql/mutations/notes/update/note.rb
+++ b/app/graphql/mutations/notes/update/note.rb
@@ -8,9 +8,14 @@ module Mutations
argument :body,
GraphQL::STRING_TYPE,
- required: true,
+ required: false,
description: copy_field_description(Types::Notes::NoteType, :body)
+ argument :confidential,
+ GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'The confidentiality flag of a note. Default is false.'
+
private
def pre_update_checks!(note, _args)
diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb
index 89c21486a74..a068fd806f5 100644
--- a/app/graphql/mutations/snippets/create.rb
+++ b/app/graphql/mutations/snippets/create.rb
@@ -40,8 +40,8 @@ module Mutations
required: false,
description: 'The paths to files uploaded in the snippet description'
- argument :files, [Types::Snippets::FileInputType],
- description: "The snippet files to create",
+ argument :blob_actions, [Types::Snippets::BlobActionInputType],
+ description: 'Actions to perform over the snippet repository and blobs',
required: false
def resolve(args)
@@ -85,9 +85,9 @@ module Mutations
def create_params(args)
args.tap do |create_args|
- # We need to rename `files` into `snippet_actions` because
+ # We need to rename `blob_actions` into `snippet_actions` because
# it's the expected key param
- create_args[:snippet_actions] = create_args.delete(:files)&.map(&:to_h)
+ create_args[:snippet_actions] = create_args.delete(:blob_actions)&.map(&:to_h)
# We need to rename `uploaded_files` into `files` because
# it's the expected key param
diff --git a/app/graphql/mutations/snippets/update.rb b/app/graphql/mutations/snippets/update.rb
index 8890158b0df..6ff632ec008 100644
--- a/app/graphql/mutations/snippets/update.rb
+++ b/app/graphql/mutations/snippets/update.rb
@@ -30,8 +30,8 @@ module Mutations
description: 'The visibility level of the snippet',
required: false
- argument :files, [Types::Snippets::FileInputType],
- description: 'The snippet files to update',
+ argument :blob_actions, [Types::Snippets::BlobActionInputType],
+ description: 'Actions to perform over the snippet repository and blobs',
required: false
def resolve(args)
@@ -56,9 +56,9 @@ module Mutations
def update_params(args)
args.tap do |update_args|
- # We need to rename `files` into `snippet_actions` because
+ # We need to rename `blob_actions` into `snippet_actions` because
# it's the expected key param
- update_args[:snippet_actions] = update_args.delete(:files)&.map(&:to_h)
+ update_args[:snippet_actions] = update_args.delete(:blob_actions)&.map(&:to_h)
end
end
end
diff --git a/app/graphql/resolvers/board_list_issues_resolver.rb b/app/graphql/resolvers/board_list_issues_resolver.rb
new file mode 100644
index 00000000000..a7cc367379d
--- /dev/null
+++ b/app/graphql/resolvers/board_list_issues_resolver.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class BoardListIssuesResolver < BaseResolver
+ type Types::IssueType, null: true
+
+ alias_method :list, :object
+
+ def resolve(**args)
+ service = Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], { board_id: list.board.id, id: list.id })
+ Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(service.execute)
+ end
+
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/235681
+ def self.complexity_multiplier(args)
+ 0.005
+ end
+ end
+end
diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb
index f8d62ba86af..b1d43934f24 100644
--- a/app/graphql/resolvers/board_lists_resolver.rb
+++ b/app/graphql/resolvers/board_lists_resolver.rb
@@ -6,12 +6,16 @@ module Resolvers
type Types::BoardListType, null: true
+ argument :id, GraphQL::ID_TYPE,
+ required: false,
+ description: 'Find a list by its global ID'
+
alias_method :board, :object
- def resolve(lookahead: nil)
+ def resolve(lookahead: nil, id: nil)
authorize!(board)
- lists = board_lists
+ lists = board_lists(id)
if load_preferences?(lookahead)
List.preload_preferences_for_user(lists, context[:current_user])
@@ -22,8 +26,13 @@ module Resolvers
private
- def board_lists
- service = Boards::Lists::ListService.new(board.resource_parent, context[:current_user])
+ def board_lists(id)
+ service = Boards::Lists::ListService.new(
+ board.resource_parent,
+ context[:current_user],
+ list_id: extract_list_id(id)
+ )
+
service.execute(board, create_default_lists: false)
end
@@ -34,5 +43,11 @@ module Resolvers
def load_preferences?(lookahead)
lookahead&.selection(:edges)&.selection(:node)&.selects?(:collapsed)
end
+
+ def extract_list_id(gid)
+ return unless gid.present?
+
+ GitlabSchema.parse_gid(gid, expected_type: ::List).model_id
+ end
end
end
diff --git a/app/graphql/resolvers/ci/pipeline_stages_resolver.rb b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb
new file mode 100644
index 00000000000..f9817d8b97b
--- /dev/null
+++ b/app/graphql/resolvers/ci/pipeline_stages_resolver.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class PipelineStagesResolver < BaseResolver
+ include LooksAhead
+
+ alias_method :pipeline, :object
+
+ def resolve_with_lookahead
+ apply_lookahead(pipeline.stages)
+ end
+
+ def preloads
+ {
+ statuses: [:needs]
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci_configuration/sast_resolver.rb b/app/graphql/resolvers/ci_configuration/sast_resolver.rb
deleted file mode 100644
index e8c42076ea2..00000000000
--- a/app/graphql/resolvers/ci_configuration/sast_resolver.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require "json"
-
-module Resolvers
- module CiConfiguration
- class SastResolver < BaseResolver
- SAST_UI_SCHEMA_PATH = 'app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json'
-
- type ::Types::CiConfiguration::Sast::Type, null: true
-
- def resolve(**args)
- Gitlab::Json.parse(File.read(Rails.root.join(SAST_UI_SCHEMA_PATH)))
- end
- end
- end
-end
diff --git a/app/graphql/resolvers/concerns/issue_resolver_fields.rb b/app/graphql/resolvers/concerns/issue_resolver_fields.rb
new file mode 100644
index 00000000000..bf2f510dd89
--- /dev/null
+++ b/app/graphql/resolvers/concerns/issue_resolver_fields.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module IssueResolverFields
+ extend ActiveSupport::Concern
+
+ prepended do
+ argument :iid, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'IID of the issue. For example, "1"'
+ argument :iids, [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'List of IIDs of issues. For example, [1, 2]'
+ argument :label_name, GraphQL::STRING_TYPE.to_list_type,
+ required: false,
+ description: 'Labels applied to this issue'
+ argument :milestone_title, GraphQL::STRING_TYPE.to_list_type,
+ required: false,
+ description: 'Milestone applied to this issue'
+ argument :assignee_username, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Username of a user assigned to the issue'
+ argument :assignee_id, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'ID of a user assigned to the issues, "none" and "any" values supported'
+ argument :created_before, Types::TimeType,
+ required: false,
+ description: 'Issues created before this date'
+ argument :created_after, Types::TimeType,
+ required: false,
+ description: 'Issues created after this date'
+ argument :updated_before, Types::TimeType,
+ required: false,
+ description: 'Issues updated before this date'
+ argument :updated_after, Types::TimeType,
+ required: false,
+ description: 'Issues updated after this date'
+ argument :closed_before, Types::TimeType,
+ required: false,
+ description: 'Issues closed before this date'
+ argument :closed_after, Types::TimeType,
+ required: false,
+ description: 'Issues closed after this date'
+ argument :search, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Search query for issue title or description'
+ argument :types, [Types::IssueTypeEnum],
+ as: :issue_types,
+ description: 'Filter issues by the given issue types',
+ required: false
+ end
+
+ def resolve(**args)
+ # The project could have been loaded in batch by `BatchLoader`.
+ # At this point we need the `id` of the project to query for issues, so
+ # make sure it's loaded and not `nil` before continuing.
+ parent = object.respond_to?(:sync) ? object.sync : object
+ return Issue.none if parent.nil?
+
+ # Will need to be made group & namespace aware with
+ # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520
+ args[:iids] ||= [args.delete(:iid)].compact if args[:iid]
+ args[:attempt_project_search_optimizations] = true if args[:search].present?
+
+ finder = IssuesFinder.new(current_user, args)
+
+ continue_issue_resolve(parent, finder, **args)
+ end
+
+ class_methods do
+ def resolver_complexity(args, child_complexity:)
+ complexity = super
+ complexity += 2 if args[:labelName]
+
+ complexity
+ end
+ end
+end
diff --git a/app/graphql/resolvers/concerns/resolves_merge_requests.rb b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
index 7ed88be52b9..0c01efd4f9a 100644
--- a/app/graphql/resolvers/concerns/resolves_merge_requests.rb
+++ b/app/graphql/resolvers/concerns/resolves_merge_requests.rb
@@ -38,6 +38,9 @@ module ResolvesMergeRequests
assignees: [:assignees],
labels: [:labels],
author: [:author],
+ merged_at: [:metrics],
+ commit_count: [:metrics],
+ approved_by: [:approver_users],
milestone: [:milestone],
head_pipeline: [:merge_request_diff, { head_pipeline: [:merge_request] }]
}
diff --git a/app/graphql/resolvers/design_management/designs_resolver.rb b/app/graphql/resolvers/design_management/designs_resolver.rb
index 81f94d5cb30..955ea6304e0 100644
--- a/app/graphql/resolvers/design_management/designs_resolver.rb
+++ b/app/graphql/resolvers/design_management/designs_resolver.rb
@@ -27,19 +27,20 @@ module Resolvers
current_user,
ids: design_ids(ids),
filenames: filenames,
- visible_at_version: version(at_version),
- order: :id
+ visible_at_version: version(at_version)
).execute
end
private
def version(at_version)
- GitlabSchema.object_from_id(at_version)&.sync if at_version
+ return unless at_version
+
+ GitlabSchema.object_from_id(at_version, expected_type: ::DesignManagement::Version)&.sync
end
def design_ids(ids)
- ids&.map { |id| GlobalID.parse(id).model_id }
+ ids&.map { |id| GlobalID.parse(id, expected_type: ::DesignManagement::Design).model_id }
end
def issue
diff --git a/app/graphql/resolvers/group_issues_resolver.rb b/app/graphql/resolvers/group_issues_resolver.rb
new file mode 100644
index 00000000000..ac51011eea8
--- /dev/null
+++ b/app/graphql/resolvers/group_issues_resolver.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class GroupIssuesResolver < IssuesResolver
+ argument :include_subgroups, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ default_value: false,
+ description: 'Include issues belonging to subgroups.'
+ end
+end
diff --git a/app/graphql/resolvers/group_milestones_resolver.rb b/app/graphql/resolvers/group_milestones_resolver.rb
new file mode 100644
index 00000000000..8d34cea4fa1
--- /dev/null
+++ b/app/graphql/resolvers/group_milestones_resolver.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class GroupMilestonesResolver < MilestonesResolver
+ argument :include_descendants, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'Also return milestones in all subgroups and subprojects'
+
+ private
+
+ def parent_id_parameters(args)
+ return { group_ids: parent.id } unless args[:include_descendants].present?
+
+ {
+ group_ids: parent.self_and_descendants.public_or_visible_to_user(current_user).select(:id),
+ project_ids: group_projects.with_issues_or_mrs_available_for_user(current_user)
+ }
+ end
+
+ def group_projects
+ GroupProjectsFinder.new(
+ group: parent,
+ current_user: current_user,
+ options: { include_subgroups: true }
+ ).execute
+ end
+ end
+end
diff --git a/app/graphql/resolvers/issue_status_counts_resolver.rb b/app/graphql/resolvers/issue_status_counts_resolver.rb
new file mode 100644
index 00000000000..466ca538467
--- /dev/null
+++ b/app/graphql/resolvers/issue_status_counts_resolver.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class IssueStatusCountsResolver < BaseResolver
+ prepend IssueResolverFields
+
+ type Types::IssueStatusCountsType, null: true
+
+ def continue_issue_resolve(parent, finder, **args)
+ Gitlab::IssuablesCountForState.new(finder, parent)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 9d0535a208f..e2874f6643c 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -2,49 +2,11 @@
module Resolvers
class IssuesResolver < BaseResolver
- argument :iid, GraphQL::STRING_TYPE,
- required: false,
- description: 'IID of the issue. For example, "1"'
+ prepend IssueResolverFields
- argument :iids, [GraphQL::STRING_TYPE],
- required: false,
- description: 'List of IIDs of issues. For example, [1, 2]'
argument :state, Types::IssuableStateEnum,
required: false,
description: 'Current state of this issue'
- argument :label_name, GraphQL::STRING_TYPE.to_list_type,
- required: false,
- description: 'Labels applied to this issue'
- argument :milestone_title, GraphQL::STRING_TYPE.to_list_type,
- required: false,
- description: 'Milestones applied to this issue'
- argument :assignee_username, GraphQL::STRING_TYPE,
- required: false,
- description: 'Username of a user assigned to the issues'
- argument :assignee_id, GraphQL::STRING_TYPE,
- required: false,
- description: 'ID of a user assigned to the issues, "none" and "any" values supported'
- argument :created_before, Types::TimeType,
- required: false,
- description: 'Issues created before this date'
- argument :created_after, Types::TimeType,
- required: false,
- description: 'Issues created after this date'
- argument :updated_before, Types::TimeType,
- required: false,
- description: 'Issues updated before this date'
- argument :updated_after, Types::TimeType,
- required: false,
- description: 'Issues updated after this date'
- argument :closed_before, Types::TimeType,
- required: false,
- description: 'Issues closed before this date'
- argument :closed_after, Types::TimeType,
- required: false,
- description: 'Issues closed after this date'
- argument :search, GraphQL::STRING_TYPE,
- required: false,
- description: 'Search query for issue title or description'
argument :sort, Types::IssueSortEnum,
description: 'Sort issues by this criteria',
required: false,
@@ -56,19 +18,7 @@ module Resolvers
label_priority_asc label_priority_desc
milestone_due_asc milestone_due_desc].freeze
- def resolve(**args)
- # The project could have been loaded in batch by `BatchLoader`.
- # At this point we need the `id` of the project to query for issues, so
- # make sure it's loaded and not `nil` before continuing.
- parent = object.respond_to?(:sync) ? object.sync : object
- return Issue.none if parent.nil?
-
- # Will need to be be made group & namespace aware with
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520
- args[:iids] ||= [args.delete(:iid)].compact if args[:iid]
- args[:attempt_project_search_optimizations] = true if args[:search].present?
-
- finder = IssuesFinder.new(current_user, args)
+ def continue_issue_resolve(parent, finder, **args)
issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all
if non_stable_cursor_sort?(args[:sort])
@@ -80,13 +30,6 @@ module Resolvers
end
end
- def self.resolver_complexity(args, child_complexity:)
- complexity = super
- complexity += 2 if args[:labelName]
-
- complexity
- end
-
def non_stable_cursor_sort?(sort)
NON_STABLE_CURSOR_SORTS.include?(sort)
end
diff --git a/app/graphql/resolvers/merge_requests_resolver.rb b/app/graphql/resolvers/merge_requests_resolver.rb
index 3aa52341eec..d15a1ede6fe 100644
--- a/app/graphql/resolvers/merge_requests_resolver.rb
+++ b/app/graphql/resolvers/merge_requests_resolver.rb
@@ -28,6 +28,12 @@ module Resolvers
required: false,
as: :label_name,
description: 'Array of label names. All resolved merge requests will have all of these labels.'
+ argument :merged_after, Types::TimeType,
+ required: false,
+ description: 'Merge requests merged after this date'
+ argument :merged_before, Types::TimeType,
+ required: false,
+ description: 'Merge requests merged before this date'
def self.single
::Resolvers::MergeRequestResolver
diff --git a/app/graphql/resolvers/milestone_resolver.rb b/app/graphql/resolvers/milestone_resolver.rb
deleted file mode 100644
index bcfbc63c31f..00000000000
--- a/app/graphql/resolvers/milestone_resolver.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-module Resolvers
- class MilestoneResolver < BaseResolver
- include Gitlab::Graphql::Authorize::AuthorizeResource
- include TimeFrameArguments
-
- argument :state, Types::MilestoneStateEnum,
- required: false,
- description: 'Filter milestones by state'
-
- argument :include_descendants, GraphQL::BOOLEAN_TYPE,
- required: false,
- description: 'Return also milestones in all subgroups and subprojects'
-
- type Types::MilestoneType, null: true
-
- def resolve(**args)
- validate_timeframe_params!(args)
-
- authorize!
-
- MilestonesFinder.new(milestones_finder_params(args)).execute
- end
-
- private
-
- def milestones_finder_params(args)
- {
- state: args[:state] || 'all',
- start_date: args[:start_date],
- end_date: args[:end_date]
- }.merge(parent_id_parameter(args))
- end
-
- def parent
- @parent ||= object.respond_to?(:sync) ? object.sync : object
- end
-
- def parent_id_parameter(args)
- if parent.is_a?(Group)
- group_parameters(args)
- elsif parent.is_a?(Project)
- { project_ids: parent.id }
- end
- end
-
- # MilestonesFinder does not check for current_user permissions,
- # so for now we need to keep it here.
- def authorize!
- Ability.allowed?(context[:current_user], :read_milestone, parent) || raise_resource_not_available_error!
- end
-
- def group_parameters(args)
- return { group_ids: parent.id } unless args[:include_descendants].present?
-
- {
- group_ids: parent.self_and_descendants.public_or_visible_to_user(current_user).select(:id),
- project_ids: group_projects.with_issues_or_mrs_available_for_user(current_user)
- }
- end
-
- def group_projects
- GroupProjectsFinder.new(
- group: parent,
- current_user: current_user,
- options: { include_subgroups: true }
- ).execute
- end
- end
-end
diff --git a/app/graphql/resolvers/milestones_resolver.rb b/app/graphql/resolvers/milestones_resolver.rb
new file mode 100644
index 00000000000..5f80506c01b
--- /dev/null
+++ b/app/graphql/resolvers/milestones_resolver.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class MilestonesResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include TimeFrameArguments
+
+ argument :ids, [GraphQL::ID_TYPE],
+ required: false,
+ description: 'Array of global milestone IDs, e.g., "gid://gitlab/Milestone/1"'
+
+ argument :state, Types::MilestoneStateEnum,
+ required: false,
+ description: 'Filter milestones by state'
+
+ type Types::MilestoneType, null: true
+
+ def resolve(**args)
+ validate_timeframe_params!(args)
+
+ authorize!
+
+ MilestonesFinder.new(milestones_finder_params(args)).execute
+ end
+
+ private
+
+ def milestones_finder_params(args)
+ {
+ ids: parse_gids(args[:ids]),
+ state: args[:state] || 'all',
+ start_date: args[:start_date],
+ end_date: args[:end_date]
+ }.merge(parent_id_parameters(args))
+ end
+
+ def parent
+ synchronized_object
+ end
+
+ def parent_id_parameters(args)
+ raise NotImplementedError
+ end
+
+ # MilestonesFinder does not check for current_user permissions,
+ # so for now we need to keep it here.
+ def authorize!
+ Ability.allowed?(context[:current_user], :read_milestone, parent) || raise_resource_not_available_error!
+ end
+
+ def parse_gids(gids)
+ gids&.map { |gid| GitlabSchema.parse_gid(gid, expected_type: Milestone).model_id }
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_milestones_resolver.rb b/app/graphql/resolvers/project_milestones_resolver.rb
new file mode 100644
index 00000000000..976fc300b87
--- /dev/null
+++ b/app/graphql/resolvers/project_milestones_resolver.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class ProjectMilestonesResolver < MilestonesResolver
+ argument :include_ancestors, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: "Also return milestones in the project's parent group and its ancestors"
+
+ private
+
+ def parent_id_parameters(args)
+ return { project_ids: parent.id } unless args[:include_ancestors].present? && parent.group.present?
+
+ {
+ group_ids: parent.group.self_and_ancestors.select(:id),
+ project_ids: parent.id
+ }
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb
index 5bafe3dd140..181c1e77109 100644
--- a/app/graphql/resolvers/project_pipeline_resolver.rb
+++ b/app/graphql/resolvers/project_pipeline_resolver.rb
@@ -10,7 +10,7 @@ module Resolvers
def resolve(iid:)
BatchLoader::GraphQL.for(iid).batch(key: project) do |iids, loader, args|
- args[:key].ci_pipelines.for_iid(iids).each { |pl| loader.call(pl.iid.to_s, pl) }
+ args[:key].all_pipelines.for_iid(iids).each { |pl| loader.call(pl.iid.to_s, pl) }
end
end
end
diff --git a/app/graphql/resolvers/projects/jira_projects_resolver.rb b/app/graphql/resolvers/projects/jira_projects_resolver.rb
index 2dc712128cc..ed382ac82d0 100644
--- a/app/graphql/resolvers/projects/jira_projects_resolver.rb
+++ b/app/graphql/resolvers/projects/jira_projects_resolver.rb
@@ -16,7 +16,14 @@ module Resolvers
response = jira_projects(name: name)
if response.success?
- response.payload[:projects]
+ projects_array = response.payload[:projects]
+
+ GraphQL::Pagination::ArrayConnection.new(
+ projects_array,
+ # override default max_page_size to whatever the size of the response is,
+ # see https://gitlab.com/gitlab-org/gitlab/-/issues/231394
+ args.merge({ max_page_size: projects_array.size })
+ )
else
raise Gitlab::Graphql::Errors::BaseError, response.message
end
diff --git a/app/graphql/resolvers/todo_resolver.rb b/app/graphql/resolvers/todo_resolver.rb
index cff65321dc0..bd5f8f274cd 100644
--- a/app/graphql/resolvers/todo_resolver.rb
+++ b/app/graphql/resolvers/todo_resolver.rb
@@ -4,7 +4,7 @@ module Resolvers
class TodoResolver < BaseResolver
type Types::TodoType, null: true
- alias_method :user, :object
+ alias_method :target, :object
argument :action, [Types::TodoActionEnum],
required: false,
@@ -31,9 +31,10 @@ module Resolvers
description: 'The type of the todo'
def resolve(**args)
- return Todo.none if user != context[:current_user]
+ return Todo.none unless current_user.present? && target.present?
+ return Todo.none if target.is_a?(User) && target != current_user
- TodosFinder.new(user, todo_finder_params(args)).execute
+ TodosFinder.new(current_user, todo_finder_params(args)).execute
end
private
@@ -46,6 +47,15 @@ module Resolvers
author_id: args[:author_id],
action_id: args[:action],
project_id: args[:project_id]
+ }.merge(target_params)
+ end
+
+ def target_params
+ return {} unless TodosFinder::TODO_TYPES.include?(target.class.name)
+
+ {
+ type: target.class.name,
+ target_id: target.id
}
end
end
diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb
index 089d2426158..1a0b0685ffe 100644
--- a/app/graphql/types/alert_management/alert_type.rb
+++ b/app/graphql/types/alert_management/alert_type.rb
@@ -71,7 +71,7 @@ module Types
description: 'Number of events of this alert',
method: :events
- field :details,
+ field :details, # rubocop:disable Graphql/JSONType
GraphQL::Types::JSON,
null: true,
description: 'Alert details'
@@ -94,8 +94,28 @@ module Types
field :metrics_dashboard_url,
GraphQL::STRING_TYPE,
null: true,
- description: 'URL for metrics embed for the alert',
- resolve: -> (alert, _args, _context) { alert.present.metrics_dashboard_url }
+ description: 'URL for metrics embed for the alert'
+
+ field :runbook,
+ GraphQL::STRING_TYPE,
+ null: true,
+ description: 'Runbook for the alert as defined in alert details'
+
+ field :todos,
+ Types::TodoType.connection_type,
+ null: true,
+ description: 'Todos of the current user for the alert',
+ resolver: Resolvers::TodoResolver
+
+ field :details_url,
+ GraphQL::STRING_TYPE,
+ null: false,
+ description: 'The URL of the alert detail page'
+
+ field :prometheus_alert,
+ Types::PrometheusAlertType,
+ null: true,
+ description: 'The alert condition for Prometheus'
def notes
object.ordered_notes
diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb
index e94ff898807..70c0794fc90 100644
--- a/app/graphql/types/board_list_type.rb
+++ b/app/graphql/types/board_list_type.rb
@@ -3,6 +3,8 @@
module Types
# rubocop: disable Graphql/AuthorizeTypes
class BoardListType < BaseObject
+ include Gitlab::Utils::StrongMemoize
+
graphql_name 'BoardList'
description 'Represents a list for an issue board'
@@ -19,6 +21,31 @@ module Types
field :collapsed, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if list is collapsed for this user',
resolve: -> (list, _args, ctx) { list.collapsed?(ctx[:current_user]) }
+ field :issues_count, GraphQL::INT_TYPE, null: true,
+ description: 'Count of issues in the list'
+
+ field :issues, ::Types::IssueType.connection_type, null: true,
+ description: 'Board issues',
+ resolver: ::Resolvers::BoardListIssuesResolver
+
+ def issues_count
+ metadata[:size]
+ end
+
+ def total_weight
+ metadata[:total_weight]
+ end
+
+ def metadata
+ strong_memoize(:metadata) do
+ list = self.object
+ user = context[:current_user]
+
+ Boards::Issues::ListService
+ .new(list.board.resource_parent, user, board_id: list.board_id, id: list.id)
+ .metadata
+ end
+ end
end
# rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/ci/group_type.rb b/app/graphql/types/ci/group_type.rb
new file mode 100644
index 00000000000..04c0eb93068
--- /dev/null
+++ b/app/graphql/types/ci/group_type.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class GroupType < BaseObject
+ graphql_name 'CiGroup'
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the job group'
+ field :size, GraphQL::INT_TYPE, null: true,
+ description: 'Size of the group'
+ field :jobs, Ci::JobType.connection_type, null: true,
+ description: 'Jobs in group'
+ end
+ end
+end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
new file mode 100644
index 00000000000..4c18f3ffd52
--- /dev/null
+++ b/app/graphql/types/ci/job_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class JobType < BaseObject
+ graphql_name 'CiJob'
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the job'
+ field :needs, JobType.connection_type, null: true,
+ description: 'Builds that must complete before the jobs run'
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_config_source_enum.rb b/app/graphql/types/ci/pipeline_config_source_enum.rb
new file mode 100644
index 00000000000..48f88c133b4
--- /dev/null
+++ b/app/graphql/types/ci/pipeline_config_source_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class PipelineConfigSourceEnum < BaseEnum
+ ::Ci::PipelineEnums.config_sources.keys.each do |state_symbol|
+ value state_symbol.to_s.upcase, value: state_symbol.to_s
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 32050766e5b..82a9f8495ce 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -5,6 +5,8 @@ module Types
class PipelineType < BaseObject
graphql_name 'Pipeline'
+ connection_type_class(Types::CountableConnectionType)
+
authorize :read_pipeline
expose_permissions Types::PermissionTypes::Ci::Pipeline
@@ -23,6 +25,8 @@ module Types
field :detailed_status, Types::Ci::DetailedStatusType, null: false,
description: 'Detailed status of the pipeline',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
+ field :config_source, PipelineConfigSourceEnum, null: true,
+ description: "Config source of the pipeline (#{::Ci::PipelineEnums.config_sources.keys.join(', ').upcase})"
field :duration, GraphQL::INT_TYPE, null: true,
description: 'Duration of the pipeline in seconds'
field :coverage, GraphQL::FLOAT_TYPE, null: true,
@@ -37,8 +41,13 @@ module Types
description: "Timestamp of the pipeline's completion"
field :committed_at, Types::TimeType, null: true,
description: "Timestamp of the pipeline's commit"
-
- # TODO: Add triggering user as a type
+ field :stages, Types::Ci::StageType.connection_type, null: true,
+ description: 'Stages of the pipeline',
+ extras: [:lookahead],
+ resolver: Resolvers::Ci::PipelineStagesResolver
+ field :user, Types::UserType, null: true,
+ description: 'Pipeline user',
+ resolve: -> (pipeline, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, pipeline.user_id).find }
end
end
end
diff --git a/app/graphql/types/ci/stage_type.rb b/app/graphql/types/ci/stage_type.rb
new file mode 100644
index 00000000000..278c4d4d748
--- /dev/null
+++ b/app/graphql/types/ci/stage_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class StageType < BaseObject
+ graphql_name 'CiStage'
+
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the stage'
+ field :groups, Ci::GroupType.connection_type, null: true,
+ description: 'Group of jobs for the stage'
+ end
+ end
+end
diff --git a/app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb b/app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb
deleted file mode 100644
index ccd1c7dd0eb..00000000000
--- a/app/graphql/types/ci_configuration/sast/analyzers_entity_type.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- module CiConfiguration
- module Sast
- # rubocop: disable Graphql/AuthorizeTypes
- class AnalyzersEntityType < BaseObject
- graphql_name 'SastCiConfigurationAnalyzersEntity'
- description 'Represents an analyzer entity in SAST CI configuration'
-
- field :name, GraphQL::STRING_TYPE, null: true,
- description: 'Name of the analyzer.'
-
- field :label, GraphQL::STRING_TYPE, null: true,
- description: 'Analyzer label used in the config UI.'
-
- field :enabled, GraphQL::BOOLEAN_TYPE, null: true,
- description: 'Indicates whether an analyzer is enabled.'
-
- field :description, GraphQL::STRING_TYPE, null: true,
- description: 'Analyzer description that is displayed on the form.'
- end
- end
- end
-end
diff --git a/app/graphql/types/ci_configuration/sast/entity_type.rb b/app/graphql/types/ci_configuration/sast/entity_type.rb
deleted file mode 100644
index b61b582ad20..00000000000
--- a/app/graphql/types/ci_configuration/sast/entity_type.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- module CiConfiguration
- module Sast
- # rubocop: disable Graphql/AuthorizeTypes
- class EntityType < BaseObject
- graphql_name 'SastCiConfigurationEntity'
- description 'Represents an entity in SAST CI configuration'
-
- field :field, GraphQL::STRING_TYPE, null: true,
- description: 'CI keyword of entity.'
-
- field :label, GraphQL::STRING_TYPE, null: true,
- description: 'Label for entity used in the form.'
-
- field :type, GraphQL::STRING_TYPE, null: true,
- description: 'Type of the field value.'
-
- field :options, ::Types::CiConfiguration::Sast::OptionsEntityType.connection_type, null: true,
- description: 'Different possible values of the field.'
-
- field :default_value, GraphQL::STRING_TYPE, null: true,
- description: 'Default value that is used if value is empty.'
-
- field :description, GraphQL::STRING_TYPE, null: true,
- description: 'Entity description that is displayed on the form.'
-
- field :value, GraphQL::STRING_TYPE, null: true,
- description: 'Current value of the entity.'
- end
- end
- end
-end
diff --git a/app/graphql/types/ci_configuration/sast/options_entity_type.rb b/app/graphql/types/ci_configuration/sast/options_entity_type.rb
deleted file mode 100644
index 86d104a7fda..00000000000
--- a/app/graphql/types/ci_configuration/sast/options_entity_type.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- module CiConfiguration
- module Sast
- # rubocop: disable Graphql/AuthorizeTypes
- class OptionsEntityType < BaseObject
- graphql_name 'SastCiConfigurationOptionsEntity'
- description 'Represents an entity for options in SAST CI configuration'
-
- field :label, GraphQL::STRING_TYPE, null: true,
- description: 'Label of option entity.'
-
- field :value, GraphQL::STRING_TYPE, null: true,
- description: 'Value of option entity.'
- end
- end
- end
-end
diff --git a/app/graphql/types/ci_configuration/sast/type.rb b/app/graphql/types/ci_configuration/sast/type.rb
deleted file mode 100644
index 35d11584ac7..00000000000
--- a/app/graphql/types/ci_configuration/sast/type.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- module CiConfiguration
- module Sast
- # rubocop: disable Graphql/AuthorizeTypes
- class Type < BaseObject
- graphql_name 'SastCiConfiguration'
- description 'Represents a CI configuration of SAST'
-
- field :global, ::Types::CiConfiguration::Sast::EntityType.connection_type, null: true,
- description: 'List of global entities related to SAST configuration.'
-
- field :pipeline, ::Types::CiConfiguration::Sast::EntityType.connection_type, null: true,
- description: 'List of pipeline entities related to SAST configuration.'
-
- field :analyzers, ::Types::CiConfiguration::Sast::AnalyzersEntityType.connection_type, null: true,
- description: 'List of analyzers entities attached to SAST configuration.'
- end
- end
- end
-end
diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb
index be5165da545..dd4b4c3b114 100644
--- a/app/graphql/types/commit_type.rb
+++ b/app/graphql/types/commit_type.rb
@@ -17,12 +17,15 @@ module Types
markdown_field :title_html, null: true
field :description, type: GraphQL::STRING_TYPE, null: true,
description: 'Description of the commit message'
+ markdown_field :description_html, null: true
field :message, type: GraphQL::STRING_TYPE, null: true,
description: 'Raw commit message'
field :authored_date, type: Types::TimeType, null: true,
description: 'Timestamp of when the commit was authored'
field :web_url, type: GraphQL::STRING_TYPE, null: false,
description: 'Web URL of the commit'
+ field :web_path, type: GraphQL::STRING_TYPE, null: false,
+ description: 'Web path of the commit'
field :signature_html, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
description: 'Rendered HTML of the commit signature'
field :author_name, type: GraphQL::STRING_TYPE, null: true,
diff --git a/app/graphql/types/countable_connection_type.rb b/app/graphql/types/countable_connection_type.rb
new file mode 100644
index 00000000000..2538366b786
--- /dev/null
+++ b/app/graphql/types/countable_connection_type.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class CountableConnectionType < GraphQL::Types::Relay::BaseConnection
+ field :count, Integer, null: false,
+ description: 'Total count of collection'
+
+ def count
+ # rubocop: disable CodeReuse/ActiveRecord
+ relation = object.items
+
+ # sometimes relation is an Array
+ relation = relation.reorder(nil) if relation.respond_to?(:reorder)
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ if relation.try(:group_values)&.present?
+ relation.size.keys.size
+ else
+ relation.size
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
index 34a90006d03..239b26f9c38 100644
--- a/app/graphql/types/environment_type.rb
+++ b/app/graphql/types/environment_type.rb
@@ -19,5 +19,10 @@ module Types
field :metrics_dashboard, Types::Metrics::DashboardType, null: true,
description: 'Metrics dashboard schema for the environment',
resolver: Resolvers::Metrics::DashboardResolver
+
+ field :latest_opened_most_severe_alert,
+ Types::AlertManagement::AlertType,
+ null: true,
+ description: 'The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.'
end
end
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index fd7d9a9ba3d..cc8cd7c01f9 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -47,11 +47,11 @@ module Types
Types::IssueType.connection_type,
null: true,
description: 'Issues of the group',
- resolver: Resolvers::IssuesResolver
+ resolver: Resolvers::GroupIssuesResolver
field :milestones, Types::MilestoneType.connection_type, null: true,
- description: 'Find milestones',
- resolver: Resolvers::MilestoneResolver
+ description: 'Milestones of the group',
+ resolver: Resolvers::GroupMilestonesResolver
field :boards,
Types::BoardType.connection_type,
diff --git a/app/graphql/types/issuable_state_enum.rb b/app/graphql/types/issuable_state_enum.rb
index f2f6d6c6cab..543b7f8e5b2 100644
--- a/app/graphql/types/issuable_state_enum.rb
+++ b/app/graphql/types/issuable_state_enum.rb
@@ -8,5 +8,6 @@ module Types
value 'opened'
value 'closed'
value 'locked'
+ value 'all'
end
end
diff --git a/app/graphql/types/issue_connection_type.rb b/app/graphql/types/issue_connection_type.rb
deleted file mode 100644
index beed392f01a..00000000000
--- a/app/graphql/types/issue_connection_type.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- # rubocop: disable Graphql/AuthorizeTypes
- class IssueConnectionType < GraphQL::Types::Relay::BaseConnection
- field :count, Integer, null: false,
- description: 'Total count of collection'
-
- def count
- object.items.size
- end
- end
-end
diff --git a/app/graphql/types/issue_status_counts_type.rb b/app/graphql/types/issue_status_counts_type.rb
new file mode 100644
index 00000000000..f2b1ba8e655
--- /dev/null
+++ b/app/graphql/types/issue_status_counts_type.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Types
+ class IssueStatusCountsType < BaseObject
+ graphql_name 'IssueStatusCountsType'
+ description "Represents total number of issues for the represented statuses."
+
+ authorize :read_issue
+
+ def self.available_issue_states
+ @available_issue_states ||= Issue.available_states.keys.push('all')
+ end
+
+ ::Gitlab::IssuablesCountForState::STATES.each do |state|
+ next unless available_issue_states.include?(state.downcase)
+
+ field state,
+ GraphQL::INT_TYPE,
+ null: true,
+ description: "Number of issues with status #{state.upcase} for the project"
+ end
+ end
+end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 9baa0018999..0a73ce95424 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -4,7 +4,7 @@ module Types
class IssueType < BaseObject
graphql_name 'Issue'
- connection_type_class(Types::IssueConnectionType)
+ connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
@@ -97,6 +97,10 @@ module Types
field :design_collection, Types::DesignManagement::DesignCollectionType, null: true,
description: 'Collection of design images associated with this issue'
+
+ field :type, Types::IssueTypeEnum, null: true,
+ method: :issue_type,
+ description: 'Type of the issue'
end
end
diff --git a/app/graphql/types/issue_type_enum.rb b/app/graphql/types/issue_type_enum.rb
new file mode 100644
index 00000000000..7dc45f78c99
--- /dev/null
+++ b/app/graphql/types/issue_type_enum.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Types
+ class IssueTypeEnum < BaseEnum
+ graphql_name 'IssueType'
+ description 'Issue type'
+
+ ::Issue.issue_types.keys.each do |issue_type|
+ value issue_type.upcase, value: issue_type, description: "#{issue_type.titleize} issue type"
+ end
+ end
+end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index c194b467363..01b02b7976f 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -4,6 +4,8 @@ module Types
class MergeRequestType < BaseObject
graphql_name 'MergeRequest'
+ connection_type_class(Types::CountableConnectionType)
+
implements(Types::Notes::NoteableType)
authorize :read_merge_request
@@ -141,6 +143,8 @@ module Types
end
field :task_completion_status, Types::TaskCompletionStatus, null: false,
description: Types::TaskCompletionStatus.description
+ field :commit_count, GraphQL::INT_TYPE, null: true,
+ description: 'Number of commits in the merge request'
def diff_stats(path: nil)
stats = Array.wrap(object.diff_stats&.to_a)
@@ -160,5 +164,14 @@ module Types
hash.merge!(additions: status.additions, deletions: status.deletions, file_count: 1) { |_, x, y| x + y }
end
end
+
+ def commit_count
+ object&.metrics&.commits_count
+ end
+
+ def approvers
+ object.approver_users
+ end
end
end
+Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType')
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 49d51b626b2..e143d14676e 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -14,12 +14,17 @@ module Types
mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle
+ mount_mutation Mutations::Boards::Issues::IssueMoveList
+ mount_mutation Mutations::Boards::Lists::Create
+ mount_mutation Mutations::Boards::Lists::Update
mount_mutation Mutations::Branches::Create, calls_gitaly: true
mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::Discussions::ToggleResolve
+ mount_mutation Mutations::Issues::SetAssignees
mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetLocked
mount_mutation Mutations::Issues::SetDueDate
+ mount_mutation Mutations::Issues::SetSubscription
mount_mutation Mutations::Issues::Update
mount_mutation Mutations::MergeRequests::Create
mount_mutation Mutations::MergeRequests::Update
@@ -55,6 +60,7 @@ module Types
mount_mutation Mutations::JiraImport::ImportUsers
mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
+ mount_mutation Mutations::DesignManagement::Move
mount_mutation Mutations::ContainerExpirationPolicies::Update
end
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 2251a0f4e0c..5562db69de6 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -148,6 +148,16 @@ module Types
description: 'Issues of the project',
resolver: Resolvers::IssuesResolver
+ field :issue_status_counts,
+ Types::IssueStatusCountsType,
+ null: true,
+ description: 'Counts of issues by status for the project',
+ resolver: Resolvers::IssueStatusCountsResolver
+
+ field :milestones, Types::MilestoneType.connection_type, null: true,
+ description: 'Milestones of the project',
+ resolver: Resolvers::ProjectMilestonesResolver
+
field :project_members,
Types::ProjectMemberType.connection_type,
description: 'Members of the project',
@@ -159,9 +169,11 @@ module Types
description: 'Environments of the project',
resolver: Resolvers::EnvironmentsResolver
- field :sast_ci_configuration, ::Types::CiConfiguration::Sast::Type, null: true,
- description: 'SAST CI configuration for the project',
- resolver: ::Resolvers::CiConfiguration::SastResolver
+ field :environment,
+ Types::EnvironmentType,
+ null: true,
+ description: 'A single environment of the project',
+ resolver: Resolvers::EnvironmentsResolver.single
field :issue,
Types::IssueType,
diff --git a/app/graphql/types/projects/services/jira_service_type.rb b/app/graphql/types/projects/services/jira_service_type.rb
index 8bf85a14cbf..cb0712249e3 100644
--- a/app/graphql/types/projects/services/jira_service_type.rb
+++ b/app/graphql/types/projects/services/jira_service_type.rb
@@ -13,8 +13,6 @@ module Types
field :projects,
Types::Projects::Services::JiraProjectType.connection_type,
null: true,
- connection: false,
- extensions: [Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension],
description: 'List of all Jira projects fetched through Jira REST API',
resolver: Resolvers::Projects::JiraProjectsResolver
end
diff --git a/app/graphql/types/prometheus_alert_type.rb b/app/graphql/types/prometheus_alert_type.rb
new file mode 100644
index 00000000000..1d09a8dbeb7
--- /dev/null
+++ b/app/graphql/types/prometheus_alert_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ class PrometheusAlertType < BaseObject
+ graphql_name 'PrometheusAlert'
+ description 'The alert condition for Prometheus'
+
+ authorize :read_prometheus_alerts
+
+ present_using PrometheusAlertPresenter
+
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the alert condition'
+
+ field :humanized_text,
+ GraphQL::STRING_TYPE,
+ null: false,
+ description: 'The human-readable text of the alert condition'
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index b4cbd96bfdb..c04f4da70cf 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -47,6 +47,15 @@ module Types
null: false,
description: 'Fields related to design management'
+ field :milestone, ::Types::MilestoneType,
+ null: true,
+ description: 'Find a milestone',
+ resolve: -> (_obj, args, _ctx) { GitlabSchema.find_by_gid(args[:id]) } do
+ argument :id, ::Types::GlobalIDType[Milestone],
+ required: true,
+ description: 'Find a milestone by its ID'
+ end
+
field :user, Types::UserType,
null: true,
description: 'Find a user',
diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb
index 73ca3425ded..db98e62c10a 100644
--- a/app/graphql/types/snippet_type.rb
+++ b/app/graphql/types/snippet_type.rb
@@ -66,7 +66,8 @@ module Types
field :blob, type: Types::Snippets::BlobType,
description: 'Snippet blob',
calls_gitaly: true,
- null: false
+ null: false,
+ deprecated: { reason: 'Use `blobs`', milestone: '13.3' }
field :blobs, type: [Types::Snippets::BlobType],
description: 'Snippet blobs',
diff --git a/app/graphql/types/snippets/file_input_action_enum.rb b/app/graphql/types/snippets/blob_action_enum.rb
index 7785853f3a8..e3f89920f16 100644
--- a/app/graphql/types/snippets/file_input_action_enum.rb
+++ b/app/graphql/types/snippets/blob_action_enum.rb
@@ -2,9 +2,9 @@
module Types
module Snippets
- class FileInputActionEnum < BaseEnum
- graphql_name 'SnippetFileInputActionEnum'
- description 'Type of a snippet file input action'
+ class BlobActionEnum < BaseEnum
+ graphql_name 'SnippetBlobActionEnum'
+ description 'Type of a snippet blob input action'
value 'create', value: :create
value 'update', value: :update
diff --git a/app/graphql/types/snippets/file_input_type.rb b/app/graphql/types/snippets/blob_action_input_type.rb
index 85a02c8f493..ccb6ae3f2c1 100644
--- a/app/graphql/types/snippets/file_input_type.rb
+++ b/app/graphql/types/snippets/blob_action_input_type.rb
@@ -2,11 +2,11 @@
module Types
module Snippets
- class FileInputType < BaseInputObject # rubocop:disable Graphql/AuthorizeTypes
- graphql_name 'SnippetFileInputType'
+ class BlobActionInputType < BaseInputObject # rubocop:disable Graphql/AuthorizeTypes
+ graphql_name 'SnippetBlobActionInputType'
description 'Represents an action to perform over a snippet file'
- argument :action, Types::Snippets::FileInputActionEnum,
+ argument :action, Types::Snippets::BlobActionEnum,
description: 'Type of input action',
required: true
diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb
index f045a50e672..c31e4873df0 100644
--- a/app/graphql/types/time_type.rb
+++ b/app/graphql/types/time_type.rb
@@ -7,6 +7,8 @@ module Types
def self.coerce_input(value, ctx)
Time.parse(value)
+ rescue ArgumentError, TypeError => e
+ raise GraphQL::CoercionError, e.message
end
def self.coerce_result(value, ctx)
diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb
index 36cae756a0d..cc6bf7b4f00 100644
--- a/app/graphql/types/tree/blob_type.rb
+++ b/app/graphql/types/tree/blob_type.rb
@@ -12,6 +12,8 @@ module Types
field :web_url, GraphQL::STRING_TYPE, null: true,
description: 'Web URL of the blob'
+ field :web_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path of the blob'
field :lfs_oid, GraphQL::STRING_TYPE, null: true,
description: 'LFS ID of the blob',
resolve: -> (blob, args, ctx) do
diff --git a/app/graphql/types/tree/tree_entry_type.rb b/app/graphql/types/tree/tree_entry_type.rb
index 81a7a7e66ae..aff2e025761 100644
--- a/app/graphql/types/tree/tree_entry_type.rb
+++ b/app/graphql/types/tree/tree_entry_type.rb
@@ -13,6 +13,8 @@ module Types
field :web_url, GraphQL::STRING_TYPE, null: true,
description: 'Web URL for the tree entry (directory)'
+ field :web_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Web path for the tree entry (directory)'
end
# rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/user_status_type.rb b/app/graphql/types/user_status_type.rb
new file mode 100644
index 00000000000..ff277c1f8e8
--- /dev/null
+++ b/app/graphql/types/user_status_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class UserStatusType < BaseObject
+ graphql_name 'UserStatus'
+
+ markdown_field :message_html, null: true,
+ description: 'HTML of the user status message'
+ field :message, GraphQL::STRING_TYPE, null: true,
+ description: 'User status message'
+ field :emoji, GraphQL::STRING_TYPE, null: true,
+ description: 'String representation of emoji'
+ end
+end
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index ab3c84ea539..cb3575b41d1 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -18,16 +18,22 @@ module Types
description: 'Human-readable name of the user'
field :state, Types::UserStateEnum, null: false,
description: 'State of the user'
+ field :email, GraphQL::STRING_TYPE, null: true,
+ description: 'User email'
field :avatar_url, GraphQL::STRING_TYPE, null: true,
description: "URL of the user's avatar"
field :web_url, GraphQL::STRING_TYPE, null: false,
description: 'Web URL of the user'
+ field :web_path, GraphQL::STRING_TYPE, null: false,
+ description: 'Web path of the user'
field :todos, Types::TodoType.connection_type, null: false,
resolver: Resolvers::TodoResolver,
description: 'Todos of the user'
field :group_memberships, Types::GroupMemberType.connection_type, null: true,
description: 'Group memberships of the user',
method: :group_members
+ field :status, Types::UserStatusType, null: true,
+ description: 'User status'
field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
description: 'Project memberships of the user',
method: :project_members
diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb
index 8fb23f99cb3..b80152777a8 100644
--- a/app/helpers/active_sessions_helper.rb
+++ b/app/helpers/active_sessions_helper.rb
@@ -20,6 +20,6 @@ module ActiveSessionsHelper
'monitor-o'
end
- sprite_icon(icon_name, size: 16, css_class: 'gl-mt-2')
+ sprite_icon(icon_name, css_class: 'gl-mt-2')
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index e8bd5ad9b9b..41b20a1d9a0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -194,6 +194,10 @@ module ApplicationHelper
'https://' + promo_host
end
+ def contact_sales_url
+ promo_url + '/sales'
+ end
+
def support_url
Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end
@@ -231,6 +235,18 @@ module ApplicationHelper
"#{request.path}?#{options.compact.to_param}"
end
+ def use_startup_css?
+ Feature.enabled?(:startup_css) && !Rails.env.test?
+ end
+
+ def stylesheet_link_tag_defer(path)
+ if use_startup_css?
+ stylesheet_link_tag(path, media: "print")
+ else
+ stylesheet_link_tag(path, media: "all")
+ end
+ end
+
def outdated_browser?
browser.ie?
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index ddaac37e7ed..404700bb25e 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -327,7 +327,8 @@ module ApplicationSettingsHelper
:project_download_export_limit,
:group_import_limit,
:group_export_limit,
- :group_download_export_limit
+ :group_download_export_limit,
+ :wiki_page_max_content_bytes
]
end
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
index 13df53a751b..af9ab93d459 100644
--- a/app/helpers/award_emoji_helper.rb
+++ b/app/helpers/award_emoji_helper.rb
@@ -12,7 +12,7 @@ module AwardEmojiHelper
toggle_award_emoji_project_note_path(@project, awardable.id)
end
else
- url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
+ url_for([:toggle_award_emoji, @project, awardable])
end
end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index f4238e7711a..615c834c529 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -48,24 +48,40 @@ module BlobHelper
return unless blob = readable_blob(options, path, project, ref)
common_classes = "btn btn-primary js-edit-blob ml-2 #{options[:extra_class]}"
+ data = { track_event: 'click_edit', track_label: 'Edit' }
+
+ if Feature.enabled?(:web_ide_primary_edit, project.group)
+ common_classes += " btn-inverted"
+ data[:track_property] = 'secondary'
+ end
edit_button_tag(blob,
common_classes,
_('Edit'),
edit_blob_path(project, ref, path, options),
project,
- ref)
+ ref,
+ data)
end
def ide_edit_button(project = @project, ref = @ref, path = @path, blob:)
return unless blob
+ common_classes = 'btn btn-primary ide-edit-button ml-2'
+ data = { track_event: 'click_edit_ide', track_label: 'Web IDE' }
+
+ unless Feature.enabled?(:web_ide_primary_edit, project.group)
+ common_classes += " btn-inverted"
+ data[:track_property] = 'secondary'
+ end
+
edit_button_tag(blob,
- 'btn btn-inverted btn-primary ide-edit-button ml-2',
+ common_classes,
_('Web IDE'),
ide_edit_path(project, ref, path),
project,
- ref)
+ ref,
+ data)
end
def modify_file_button(project = @project, ref = @ref, path = @path, blob:, label:, action:, btn_class:, modal_type:)
@@ -184,6 +200,10 @@ module BlobHelper
@gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls, project).execute)
end
+ def metrics_dashboard_ymls(project)
+ @metrics_dashboard_ymls ||= template_dropdown_names(TemplateFinder.build(:metrics_dashboard_ymls, project).execute)
+ end
+
def dockerfile_names(project)
@dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles, project).execute)
end
@@ -325,16 +345,16 @@ module BlobHelper
button_tag(button_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' })
end
- def edit_link_tag(link_text, edit_path, common_classes)
- link_to link_text, edit_path, class: "#{common_classes} btn-sm"
+ def edit_link_tag(link_text, edit_path, common_classes, data)
+ link_to link_text, edit_path, class: "#{common_classes} btn-sm", data: data
end
- def edit_button_tag(blob, common_classes, text, edit_path, project, ref)
+ def edit_button_tag(blob, common_classes, text, edit_path, project, ref, data)
if !on_top_of_branch?(project, ref)
edit_disabled_button_tag(text, common_classes)
# This condition only applies to users who are logged in
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
- edit_link_tag(text, edit_path, common_classes)
+ edit_link_tag(text, edit_path, common_classes, data)
elsif can?(current_user, :fork_project, project) && can?(current_user, :create_merge_request_in, project)
edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
end
@@ -343,7 +363,7 @@ module BlobHelper
def show_suggest_pipeline_creation_celebration?
experiment_enabled?(:suggest_pipeline) &&
@blob.path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] &&
- @blob.auxiliary_viewer.valid?(project: @project, sha: @commit.sha, user: current_user) &&
+ @blob.auxiliary_viewer&.valid?(project: @project, sha: @commit.sha, user: current_user) &&
@project.uses_default_ci_config? &&
cookies[suggest_pipeline_commit_cookie_name].present?
end
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 60c19e6fecd..2a0242fe2fa 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -8,6 +8,14 @@ module BranchesHelper
def protected_branch?(project, branch)
ProtectedBranch.protected?(project, branch.name)
end
+
+ def access_levels_data(access_levels)
+ return [] unless access_levels
+
+ access_levels.map do |level|
+ { id: level.id, type: :role, access_level: level.access_level }
+ end
+ end
end
BranchesHelper.prepend_if_ee('EE::BranchesHelper')
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
new file mode 100644
index 00000000000..749726e0e33
--- /dev/null
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Ci
+ module PipelinesHelper
+ def pipeline_warnings(pipeline)
+ return unless pipeline.warning_messages.any?
+
+ content_tag(:div, class: 'alert alert-warning') do
+ content_tag(:h4, 'Warning:') <<
+ content_tag(:div) do
+ pipeline.warning_messages.each do |warning|
+ concat(markdown(warning.content))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index c85d2a68f14..b97e847c397 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -24,6 +24,18 @@ module ClustersHelper
}
end
+ def js_cluster_form_data(cluster, can_edit)
+ {
+ enabled: cluster.enabled?.to_s,
+ editable: can_edit.to_s,
+ environment_scope: cluster.environment_scope,
+ base_domain: cluster.base_domain,
+ application_ingress_external_ip: cluster.application_ingress_external_ip,
+ auto_devops_help_path: help_page_path('topics/autodevops/index'),
+ external_endpoint_help_path: help_page_path('user/clusters/applications.md', anchor: 'pointing-your-dns-at-the-external-endpoint')
+ }
+ end
+
# This method is depreciated and will be removed when associated HAML files are moved to JavaScript
def provider_icon(provider = nil)
img_data = js_clusters_list_data.dig(:img_tags, provider&.to_sym) ||
diff --git a/app/helpers/custom_metrics_helper.rb b/app/helpers/custom_metrics_helper.rb
index fbea6d2050f..5ea386e268d 100644
--- a/app/helpers/custom_metrics_helper.rb
+++ b/app/helpers/custom_metrics_helper.rb
@@ -2,10 +2,8 @@
module CustomMetricsHelper
def custom_metrics_data(project, metric)
- custom_metrics_path = project.namespace.becomes(::Namespace)
-
{
- 'custom-metrics-path' => url_for([custom_metrics_path, project, metric]),
+ 'custom-metrics-path' => url_for([project, metric]),
'metric-persisted' => metric.persisted?.to_s,
'edit-project-service-path' => edit_project_service_path(project, PrometheusService),
'validate-query-path' => validate_query_project_prometheus_metrics_path(project),
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index 7bf3795d73a..0ba03cd90ea 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -40,7 +40,7 @@ module DashboardHelper
end)
if doc_href.present?
- link_to_doc = link_to(sprite_icon('question', size: 16), doc_href,
+ link_to_doc = link_to(sprite_icon('question'), doc_href,
class: 'gl-ml-2', title: _('Documentation'),
target: '_blank', rel: 'noopener noreferrer')
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index bd400009c96..c4487ae8e4a 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -19,7 +19,7 @@ module EnvironmentHelper
end
def deployment_path(deployment)
- [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ [deployment.project, deployment.deployable]
end
def deployment_link(deployment, text: nil)
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index b522a9dfb4f..39be8ae9f60 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -98,20 +98,21 @@ module EnvironmentsHelper
'deployments-endpoint' => project_environment_deployments_path(project, environment, format: :json),
'alerts-endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json),
'operations-settings-path' => project_settings_operations_path(project),
- 'can-access-operations-settings' => can?(current_user, :admin_operations, project).to_s
+ 'can-access-operations-settings' => can?(current_user, :admin_operations, project).to_s,
+ 'panel-preview-endpoint' => project_metrics_dashboards_builder_path(project, format: :json)
}
end
def static_metrics_data
{
'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'),
- 'add-dashboard-documentation-path' => help_page_path('user/project/integrations/prometheus.md', anchor: 'adding-a-new-dashboard-to-your-project'),
+ 'add-dashboard-documentation-path' => help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
'empty-getting-started-svg-path' => image_path('illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path' => image_path('illustrations/monitoring/loading.svg'),
'empty-no-data-svg-path' => image_path('illustrations/monitoring/no_data.svg'),
'empty-no-data-small-svg-path' => image_path('illustrations/chart-empty-state-small.svg'),
'empty-unable-to-connect-svg-path' => image_path('illustrations/monitoring/unable_to_connect.svg'),
- 'custom-dashboard-base-path' => Metrics::Dashboard::CustomDashboardService::DASHBOARD_ROOT
+ 'custom-dashboard-base-path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT
}
end
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 207230fd92e..0167f2ef698 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -199,8 +199,7 @@ module EventsHelper
elsif event.design_note?
design_url(event.note_target, anchor: dom_id(event.note))
else
- polymorphic_url([event.project.namespace.becomes(Namespace),
- event.project, event.note_target],
+ polymorphic_url([event.project, event.note_target],
anchor: dom_id(event.target))
end
end
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index ecacde65c10..67bfeb22d92 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -11,7 +11,7 @@ module FormHelper
content_tag(:h4, headline) <<
content_tag(:ul) do
messages = model.errors.map do |attribute, message|
- message = model.errors.full_message(attribute, message)
+ message = html_escape_once(model.errors.full_message(attribute, message)).html_safe
message = content_tag(:span, message, class: 'str-truncated-100') if truncate.include?(attribute)
content_tag(:li, message)
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 04f34f5a3ae..d71e6b4c004 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -100,8 +100,12 @@ module GitlabRoutingHelper
toggle_award_emoji_snippet_path(*args)
end
- def toggle_award_emoji_namespace_project_project_snippet_path(*args)
- toggle_award_emoji_namespace_project_snippet_path(*args)
+ def toggle_award_emoji_project_project_snippet_path(*args)
+ toggle_award_emoji_project_snippet_path(*args)
+ end
+
+ def toggle_award_emoji_project_project_snippet_url(*args)
+ toggle_award_emoji_project_snippet_url(*args)
end
## Members
@@ -271,7 +275,7 @@ module GitlabRoutingHelper
end
end
- def gitlab_raw_snippet_blob_url(snippet, path, ref = nil)
+ def gitlab_raw_snippet_blob_url(snippet, path, ref = nil, **options)
params = {
snippet_id: snippet,
ref: ref || snippet.repository.root_ref,
@@ -279,26 +283,14 @@ module GitlabRoutingHelper
}
if snippet.is_a?(ProjectSnippet)
- project_snippet_blob_raw_url(snippet.project, params)
+ project_snippet_blob_raw_url(snippet.project, **params, **options)
else
- snippet_blob_raw_url(params)
+ snippet_blob_raw_url(**params, **options)
end
end
- def gitlab_raw_snippet_blob_path(blob, ref = nil)
- snippet = blob.container
-
- params = {
- snippet_id: snippet,
- ref: ref || blob.repository.root_ref,
- path: blob.path
- }
-
- if snippet.is_a?(ProjectSnippet)
- project_snippet_blob_raw_path(snippet.project, params)
- else
- snippet_blob_raw_path(params)
- end
+ def gitlab_raw_snippet_blob_path(snippet, path, ref = nil, **options)
+ gitlab_raw_snippet_blob_url(snippet, path, ref, only_path: true, **options)
end
def gitlab_snippet_notes_path(snippet, *args)
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index 49b15cde009..24072d1ab46 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -17,7 +17,7 @@ module GraphHelper
end
def success_ratio(counts)
- return 100 if counts[:failed].zero?
+ return 100 if counts[:failed] == 0
ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100
ratio.to_i
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 61c9bd74451..eb80acd869f 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -28,6 +28,7 @@ module GroupsHelper
def group_packages_nav_link_paths
%w[
+ groups/packages#index
groups/container_registries#index
]
end
@@ -129,7 +130,7 @@ module GroupsHelper
end
def remove_group_message(group)
- _("You are going to remove %{group_name}, this will also remove all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
+ _("You are going to remove %{group_name}, this will also delete all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
end
@@ -157,6 +158,15 @@ module GroupsHelper
groups.to_json
end
+ def group_packages_nav?
+ group_packages_list_nav? ||
+ group_container_registry_nav?
+ end
+
+ def group_packages_list_nav?
+ @group.packages_feature_enabled?
+ end
+
private
def get_group_sidebar_links
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index add15cc0d12..9957d5c6330 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -6,6 +6,8 @@ module IconsHelper
extend self
include FontAwesome::Rails::IconHelper
+ DEFAULT_ICON_SIZE = 16
+
# Creates an icon tag given icon name(s) and possible icon modifiers.
#
# Right now this method simply delegates directly to `fa_icon` from the
@@ -21,7 +23,7 @@ module IconsHelper
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
end
- def custom_icon(icon_name, size: 16)
+ def custom_icon(icon_name, size: DEFAULT_ICON_SIZE)
# We can't simply do the below, because there are some .erb SVGs.
# File.read(Rails.root.join("app/views/shared/icons/_#{icon_name}.svg")).html_safe
render "shared/icons/#{icon_name}.svg", size: size
@@ -43,7 +45,7 @@ module IconsHelper
ActionController::Base.helpers.image_path('file_icons.svg', host: sprite_base_url)
end
- def sprite_icon(icon_name, size: nil, css_class: nil)
+ def sprite_icon(icon_name, size: DEFAULT_ICON_SIZE, css_class: nil)
if known_sprites&.exclude?(icon_name)
exception = ArgumentError.new("#{icon_name} is not a known icon in @gitlab-org/gitlab-svg")
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception)
@@ -52,7 +54,13 @@ module IconsHelper
css_classes = []
css_classes << "s#{size}" if size
css_classes << "#{css_class}" unless css_class.blank?
- content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes.join(' '))
+
+ content_tag(
+ :svg,
+ content_tag(:use, '', { 'xlink:href' => "#{sprite_icon_path}##{icon_name}" } ),
+ class: css_classes.empty? ? nil : css_classes.join(' '),
+ data: { testid: "#{icon_name}-icon" }
+ )
end
def loading_icon(container: false, color: 'orange', size: 'sm', css_class: nil)
@@ -94,11 +102,11 @@ module IconsHelper
if value
icon('circle', class: 'cgreen')
else
- icon('power-off', class: 'clgray')
+ sprite_icon('power', css_class: 'clgray')
end
end
- def visibility_level_icon(level, fw: true, options: {})
+ def visibility_level_icon(level, options: {})
name =
case level
when Gitlab::VisibilityLevel::PRIVATE
@@ -106,13 +114,12 @@ module IconsHelper
when Gitlab::VisibilityLevel::INTERNAL
'shield'
else # Gitlab::VisibilityLevel::PUBLIC
- 'globe'
+ 'earth'
end
- name = [name]
- name << "fw" if fw
+ css_class = options.delete(:class)
- icon(name.join(' '), options)
+ sprite_icon(name, size: DEFAULT_ICON_SIZE, css_class: css_class)
end
def file_type_icon_class(type, mode, name)
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index 1ee67211ab0..329bbb5ad82 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -56,7 +56,7 @@ module ImportHelper
link_url = 'https://github.com/settings/tokens'
link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: link_url }
- _('Create and provide your GitHub %{link_start}Personal Access Token%{link_end}. You will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
+ html_escape(_('Create and provide your GitHub %{link_start}Personal Access Token%{link_end}. You will need to select the %{code_open}repo%{code_close} scope, so we can display a list of your public and private repositories which are available to import.')) % { link_start: link_start, link_end: '</a>'.html_safe, code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
end
def import_configure_github_admin_message
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index c9ba42491f3..0b859a39c4f 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -205,7 +205,7 @@ module IssuablesHelper
author_output
end
- output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!'))
+ output << content_tag(:span, (sprite_icon('first-contribution', css_class: 'gl-icon gl-vertical-align-middle') if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!'))
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block gl-ml-3")
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none")
@@ -247,13 +247,6 @@ module IssuablesHelper
html.html_safe
end
- def issuable_first_contribution_icon
- content_tag(:span, class: 'fa-stack') do
- concat(icon('certificate', class: "fa-stack-2x"))
- concat(content_tag(:strong, '1', class: 'fa-inverse fa-stack-1x'))
- end
- end
-
def assigned_issuables_count(issuable_type)
case issuable_type
when :issues
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 61fe075303c..55170cbfa6b 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -41,7 +41,7 @@ module IssuesHelper
end
def confidential_icon(issue)
- sprite_icon('eye-slash', size: 16, css_class: 'gl-vertical-align-text-bottom') if issue.confidential?
+ sprite_icon('eye-slash', css_class: 'gl-vertical-align-text-bottom') if issue.confidential?
end
def award_user_list(awards, current_user, limit: 10)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index caf39741543..1125ecb9b41 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -170,12 +170,6 @@ module MergeRequestsHelper
current_user.fork_of(project)
end
end
-
- def mr_tabs_position_enabled?
- strong_memoize(:mr_tabs_position_enabled) do
- Feature.enabled?(:mr_tabs_position, @project, default_enabled: true)
- end
- end
end
MergeRequestsHelper.prepend_if_ee('EE::MergeRequestsHelper')
diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb
index 6f6cb91e696..50fc5e521fc 100644
--- a/app/helpers/mirror_helper.rb
+++ b/app/helpers/mirror_helper.rb
@@ -9,7 +9,7 @@ module MirrorHelper
end
def mirror_lfs_sync_message
- _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
+ html_escape(_('The Git LFS objects will %{strong_open}not%{strong_close} be synced.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
end
end
diff --git a/app/helpers/namespace_storage_limit_alert_helper.rb b/app/helpers/namespace_storage_limit_alert_helper.rb
new file mode 100644
index 00000000000..d7174c38254
--- /dev/null
+++ b/app/helpers/namespace_storage_limit_alert_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module NamespaceStorageLimitAlertHelper
+ # Overridden in EE
+ def display_namespace_storage_limit_alert!
+ end
+end
+
+NamespaceStorageLimitAlertHelper.prepend_if_ee('EE::NamespaceStorageLimitAlertHelper')
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 782f1d3e759..2ba1d841c2e 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -57,12 +57,14 @@ module NotesHelper
def add_diff_note_button(line_code, position, line_type)
return if @diff_notes_disabled
- button_tag '',
- class: 'add-diff-note js-add-diff-note-button',
- type: 'submit', name: 'button',
- data: diff_view_line_data(line_code, position, line_type),
- title: _('Add a comment to this line') do
- sprite_icon('comment', size: 12)
+ content_tag(:span, class: 'add-diff-note tooltip-wrapper') do
+ button_tag '',
+ class: 'note-button add-diff-note js-add-diff-note-button',
+ type: 'submit', name: 'button',
+ data: diff_view_line_data(line_code, position, line_type),
+ title: _('Add a comment to this line') do
+ sprite_icon('comment', size: 12)
+ end
end
end
@@ -126,7 +128,7 @@ module NotesHelper
if @snippet.is_a?(PersonalSnippet)
[@note]
else
- [@project.namespace.becomes(Namespace), @project, @note]
+ [@project, @note]
end
end
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 68dfd008921..9a64fe98f86 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -120,8 +120,4 @@ module NotificationsHelper
def can_read_project?(project)
can?(current_user, :read_project, project)
end
-
- def notification_event_disabled?(event)
- event == :fixed_pipeline && !Gitlab::Ci::Features.pipeline_fixed_notifications?
- end
end
diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb
index 3444773fe88..37e91153710 100644
--- a/app/helpers/operations_helper.rb
+++ b/app/helpers/operations_helper.rb
@@ -45,7 +45,7 @@ module OperationsHelper
send_email: setting.send_email.to_s,
pagerduty_active: setting.pagerduty_active.to_s,
pagerduty_token: setting.pagerduty_token.to_s,
- pagerduty_webhook_url: project_incidents_pagerduty_url(@project, token: setting.pagerduty_token),
+ pagerduty_webhook_url: project_incidents_integrations_pagerduty_url(@project, token: setting.pagerduty_token),
pagerduty_reset_key_path: reset_pagerduty_token_project_settings_operations_path(@project)
}
end
diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb
new file mode 100644
index 00000000000..e6ecc403a88
--- /dev/null
+++ b/app/helpers/packages_helper.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module PackagesHelper
+ def package_sort_path(options = {})
+ "#{request.path}?#{options.to_param}"
+ end
+
+ def nuget_package_registry_url(project_id)
+ expose_url(api_v4_projects_packages_nuget_index_path(id: project_id, format: '.json'))
+ end
+
+ def package_registry_instance_url(registry_type)
+ expose_url("api/#{::API::API.version}/packages/#{registry_type}")
+ end
+
+ def package_registry_project_url(project_id, registry_type = :maven)
+ project_api_path = expose_path(api_v4_projects_path(id: project_id))
+ package_registry_project_path = "#{project_api_path}/packages/#{registry_type}"
+ expose_url(package_registry_project_path)
+ end
+
+ def package_from_presenter(package)
+ presenter = ::Packages::Detail::PackagePresenter.new(package)
+
+ presenter.detail_view.to_json
+ end
+
+ def pypi_registry_url(project_id)
+ full_url = expose_url(api_v4_projects_packages_pypi_simple_package_name_path({ id: project_id, package_name: '' }, true))
+ full_url.sub!('://', '://__token__:<your_personal_token>@')
+ end
+
+ def composer_registry_url(group_id)
+ expose_url(api_v4_group___packages_composer_packages_path(id: group_id, format: '.json'))
+ end
+
+ def packages_coming_soon_enabled?(resource)
+ ::Feature.enabled?(:packages_coming_soon, resource) && ::Gitlab.dev_env_or_com?
+ end
+
+ def packages_coming_soon_data(resource)
+ return unless packages_coming_soon_enabled?(resource)
+
+ {
+ project_path: ::Gitlab.com? ? 'gitlab-org/gitlab' : 'gitlab-org/gitlab-test',
+ suggested_contributions: help_page_path('user/packages/index', anchor: 'suggested-contributions')
+ }
+ end
+
+ def packages_list_data(type, resource)
+ {
+ resource_id: resource.id,
+ page_type: type,
+ empty_list_help_url: help_page_path('administration/packages/index'),
+ empty_list_illustration: image_path('illustrations/no-packages.svg'),
+ coming_soon_json: packages_coming_soon_data(resource).to_json
+ }
+ end
+end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 271359fcfd1..5a917a02d51 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -71,7 +71,7 @@ module PreferencesHelper
def language_choices
options_for_select(
- Gitlab::I18n::AVAILABLE_LANGUAGES.map(&:reverse).sort,
+ Gitlab::I18n.selectable_locales.map(&:reverse).sort,
current_user.preferred_language
)
end
diff --git a/app/helpers/product_analytics_helper.rb b/app/helpers/product_analytics_helper.rb
new file mode 100644
index 00000000000..b040a8581b2
--- /dev/null
+++ b/app/helpers/product_analytics_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module ProductAnalyticsHelper
+ def product_analytics_tracker_url
+ ProductAnalytics::Tracker::URL
+ end
+
+ def product_analytics_tracker_collector_url
+ ProductAnalytics::Tracker::COLLECTOR_URL
+ end
+end
diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb
index d6e8e738a1c..c2f0b8854e1 100644
--- a/app/helpers/projects/alert_management_helper.rb
+++ b/app/helpers/projects/alert_management_helper.rb
@@ -5,7 +5,8 @@ module Projects::AlertManagementHelper
{
'project-path' => project.full_path,
'enable-alert-management-path' => project_settings_operations_path(project, anchor: 'js-alert-management-settings'),
- 'populating-alerts-help-url' => help_page_url('user/project/operations/alert_management.html', anchor: 'enable-alert-management'),
+ 'alerts-help-url' => help_page_url('operations/incident_management/index.md'),
+ 'populating-alerts-help-url' => help_page_url('operations/incident_management/index.md', anchor: 'enable-alert-management'),
'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => can?(current_user, :admin_operations, project).to_s,
'alert-management-enabled' => alert_management_enabled?(project).to_s
diff --git a/app/helpers/projects/incidents_helper.rb b/app/helpers/projects/incidents_helper.rb
new file mode 100644
index 00000000000..e96f0f5a384
--- /dev/null
+++ b/app/helpers/projects/incidents_helper.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Projects::IncidentsHelper
+ def incidents_data(project)
+ {
+ 'project-path' => project.full_path,
+ 'new-issue-path' => new_project_issue_path(project),
+ 'incident-template-name' => 'incident',
+ 'incident-type' => 'incident',
+ 'issue-path' => project_issues_path(project),
+ 'empty-list-svg-path' => image_path('illustrations/incident-empty-state.svg')
+ }
+ end
+end
+
+Projects::IncidentsHelper.prepend_if_ee('EE::Projects::IncidentsHelper')
diff --git a/app/helpers/projects/issues/service_desk_helper.rb b/app/helpers/projects/issues/service_desk_helper.rb
new file mode 100644
index 00000000000..0f87e5ed837
--- /dev/null
+++ b/app/helpers/projects/issues/service_desk_helper.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Projects::Issues::ServiceDeskHelper
+ def service_desk_meta(project)
+ empty_state_meta = {
+ is_service_desk_supported: Gitlab::ServiceDesk.supported?,
+ is_service_desk_enabled: project.service_desk_enabled?,
+ can_edit_project_settings: can?(current_user, :admin_project, project)
+ }
+
+ if Gitlab::ServiceDesk.supported?
+ empty_state_meta.merge(supported_meta(project))
+ else
+ empty_state_meta.merge(unsupported_meta(project))
+ end
+ end
+
+ private
+
+ def supported_meta(project)
+ {
+ service_desk_address: project.service_desk_address,
+ service_desk_help_page: help_page_path('user/project/service_desk'),
+ edit_project_page: edit_project_path(project),
+ svg_path: image_path('illustrations/service_desk_empty.svg')
+ }
+ end
+
+ def unsupported_meta(project)
+ {
+ incoming_email_help_page: help_page_path('administration/incoming_email', anchor: 'set-it-up'),
+ svg_path: image_path('illustrations/service-desk-setup.svg')
+ }
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 840e3ef9daa..1ce4903f8df 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -104,7 +104,7 @@ module ProjectsHelper
end
def remove_project_message(project)
- _("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
+ _("You are going to delete %{project_full_name}. Deleted projects CANNOT be restored! Are you ABSOLUTELY sure?") %
{ project_full_name: project.full_name }
end
@@ -184,9 +184,8 @@ module ProjectsHelper
end
def autodeploy_flash_notice(branch_name)
- translation = _("Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}") %
- { branch_name: truncate(sanitize(branch_name)), link_to_autodeploy_doc: link_to_autodeploy_doc }
- translation.html_safe
+ html_escape(_("Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}")) %
+ { branch_name: tag.strong(truncate(sanitize(branch_name))), link_to_autodeploy_doc: link_to_autodeploy_doc }
end
def project_list_cache_key(project, pipeline_status: true)
@@ -353,14 +352,14 @@ module ProjectsHelper
description =
if share_with_group && share_with_members
- _("You can invite a new member to <strong>%{project_name}</strong> or invite another group.")
+ _("You can invite a new member to %{project_name} or invite another group.")
elsif share_with_group
- _("You can invite another group to <strong>%{project_name}</strong>.")
+ _("You can invite another group to %{project_name}.")
elsif share_with_members
- _("You can invite a new member to <strong>%{project_name}</strong>.")
+ _("You can invite a new member to %{project_name}.")
end
- description.html_safe % { project_name: project.name }
+ html_escape(description) % { project_name: tag.strong(project.name) }
end
def metrics_external_dashboard_url
@@ -421,6 +420,10 @@ module ProjectsHelper
nav_tabs << :operations
end
+ if can_view_product_analytics?(current_user, project)
+ nav_tabs << :product_analytics
+ end
+
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
@@ -429,9 +432,19 @@ module ProjectsHelper
apply_external_nav_tabs(nav_tabs, project)
+ nav_tabs += package_nav_tabs(project, current_user)
+
nav_tabs
end
+ def package_nav_tabs(project, current_user)
+ [].tap do |tabs|
+ if ::Gitlab.config.packages.enabled && can?(current_user, :read_package, project)
+ tabs << :packages
+ end
+ end
+ end
+
def apply_external_nav_tabs(nav_tabs, project)
nav_tabs << :external_issue_tracker if project.external_issue_tracker
nav_tabs << :external_wiki if project.external_wiki
@@ -455,6 +468,7 @@ module ProjectsHelper
serverless: :read_cluster,
error_tracking: :read_sentry_issue,
alert_management: :read_alert_management_alert,
+ incidents: :read_incidents,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
@@ -468,6 +482,11 @@ module ProjectsHelper
end
end
+ def can_view_product_analytics?(current_user, project)
+ Feature.enabled?(:product_analytics, project) &&
+ can?(current_user, :read_product_analytics, project)
+ end
+
def search_tab_ability_map
@search_tab_ability_map ||= tab_ability_map.merge(
blobs: :download_code,
@@ -584,6 +603,7 @@ module ProjectsHelper
def project_permissions_settings(project)
feature = project.project_feature
{
+ packagesEnabled: !!project.packages_enabled,
visibilityLevel: project.visibility_level,
requestAccessEnabled: !!project.request_access_enabled,
issuesAccessLevel: feature.issues_access_level,
@@ -604,6 +624,8 @@ module ProjectsHelper
def project_permissions_panel_data(project)
{
+ packagesAvailable: ::Gitlab.config.packages.enabled,
+ packagesHelpPath: help_page_path('user/packages/index'),
currentSettings: project_permissions_settings(project),
canDisableEmails: can_disable_emails?(project, current_user),
canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
@@ -719,9 +741,13 @@ module ProjectsHelper
functions
error_tracking
alert_management
+ incidents
+ incident_management
user
gcp
logs
+ product_analytics
+ metrics_dashboard
]
end
@@ -748,7 +774,7 @@ module ProjectsHelper
def project_access_token_available?(project)
return false if ::Gitlab.com?
- ::Feature.enabled?(:resource_access_token, project)
+ ::Feature.enabled?(:resource_access_token, project, default_enabled: true)
end
end
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index a3d944c64cc..f1dff18523f 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -19,7 +19,7 @@ module ReleasesHelper
documentation_path: help_page
}.tap do |data|
if can?(current_user, :create_release, @project)
- data[:new_release_path] = if Feature.enabled?(:new_release_page, @project)
+ data[:new_release_path] = if Feature.enabled?(:new_release_page, @project, default_enabled: true)
new_project_release_path(@project)
else
new_project_tag_path(@project)
@@ -37,7 +37,8 @@ module ReleasesHelper
def data_for_new_release_page
new_edit_pages_shared_data.merge(
- default_branch: @project.default_branch
+ default_branch: @project.default_branch,
+ releases_page_path: project_releases_path(@project)
)
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 1b9876b9a6a..377aee1ae9e 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -229,6 +229,7 @@ module SearchHelper
opts[:data]['group-id'] = @group.id
opts[:data]['labels-endpoint'] = group_labels_path(@group)
opts[:data]['milestones-endpoint'] = group_milestones_path(@group)
+ opts[:data]['releases-endpoint'] = group_releases_path(@group)
else
opts[:data]['labels-endpoint'] = dashboard_labels_path
opts[:data]['milestones-endpoint'] = dashboard_milestones_path
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 1f9cce80bed..e9d39cc8175 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -35,19 +35,6 @@ module ServicesHelper
"#{event}_events"
end
- def service_event_action_field_name(action)
- "#{action}_on_event_enabled"
- end
-
- def event_action_title(action)
- case action
- when "comment"
- s_("ProjectService|Comment")
- else
- action.humanize
- end
- end
-
def service_save_button(disabled: false)
button_tag(class: 'btn btn-success', type: 'submit', disabled: disabled, data: { qa_selector: 'save_changes_button' }) do
icon('spinner spin', class: 'hidden js-btn-spinner') +
@@ -95,10 +82,6 @@ module ServicesHelper
end
end
- def integration_form_refactor?
- Feature.enabled?(:integration_form_refactor, @project, default_enabled: true)
- end
-
def integration_form_data(integration)
{
id: integration.id,
@@ -116,23 +99,13 @@ module ServicesHelper
end
def trigger_events_for_service(integration)
- return [] unless integration_form_refactor?
-
ServiceEventSerializer.new(service: integration).represent(integration.configurable_events).to_json
end
def fields_for_service(integration)
- return [] unless integration_form_refactor?
-
ServiceFieldSerializer.new(service: integration).represent(integration.global_fields).to_json
end
- def show_service_trigger_events?(integration)
- return false if integration.is_a?(JiraService) || integration_form_refactor?
-
- integration.configurable_events.present?
- end
-
def project_jira_issues_integration?
false
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index d6a9e447fbc..10c95da394f 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -60,9 +60,9 @@ module SnippetsHelper
def snippet_badge(snippet)
return unless attrs = snippet_badge_attributes(snippet)
- css_class, text = attrs
+ icon_name, text = attrs
tag.span(class: %w[badge badge-gray]) do
- concat(tag.i(class: ['fa', css_class]))
+ concat(sprite_icon(icon_name, size: 14, css_class: 'gl-vertical-align-middle'))
concat(' ')
concat(text)
end
@@ -70,25 +70,24 @@ module SnippetsHelper
def snippet_badge_attributes(snippet)
if snippet.private?
- ['fa-lock', _('private')]
+ ['lock', _('private')]
end
end
- def embedded_raw_snippet_button
- blob = @snippet.blob
+ def embedded_raw_snippet_button(snippet, blob)
return if blob.empty? || blob.binary? || blob.stored_externally?
link_to(external_snippet_icon('doc-code'),
- gitlab_raw_snippet_url(@snippet),
+ gitlab_raw_snippet_blob_url(snippet, blob.path),
class: 'btn',
target: '_blank',
rel: 'noopener noreferrer',
title: 'Open raw')
end
- def embedded_snippet_download_button
+ def embedded_snippet_download_button(snippet, blob)
link_to(external_snippet_icon('download'),
- gitlab_raw_snippet_url(@snippet, inline: false),
+ gitlab_raw_snippet_blob_url(snippet, blob.path, nil, inline: false),
class: 'btn',
target: '_blank',
title: 'Download',
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index ed1b35338ae..de6990041a6 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -238,7 +238,7 @@ module SortingHelper
end
link_to(url, type: 'button', class: link_class, title: s_('SortOptions|Sort direction')) do
- sprite_icon(icon, size: 16)
+ sprite_icon(icon)
end
end
@@ -581,6 +581,47 @@ module SortingHelper
def sort_value_expire_date
'expired_asc'
end
+
+ def packages_sort_options_hash
+ {
+ sort_value_recently_created => sort_title_created_date,
+ sort_value_oldest_created => sort_title_created_date,
+ sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name,
+ sort_value_version_desc => sort_title_version,
+ sort_value_version_asc => sort_title_version,
+ sort_value_type_desc => sort_title_type,
+ sort_value_type_asc => sort_title_type,
+ sort_value_project_name_desc => sort_title_project_name,
+ sort_value_project_name_asc => sort_title_project_name
+ }
+ end
+
+ def packages_reverse_sort_order_hash
+ {
+ sort_value_recently_created => sort_value_oldest_created,
+ sort_value_oldest_created => sort_value_recently_created,
+ sort_value_name => sort_value_name_desc,
+ sort_value_name_desc => sort_value_name,
+ sort_value_version_desc => sort_value_version_asc,
+ sort_value_version_asc => sort_value_version_desc,
+ sort_value_type_desc => sort_value_type_asc,
+ sort_value_type_asc => sort_value_type_desc,
+ sort_value_project_name_desc => sort_value_project_name_asc,
+ sort_value_project_name_asc => sort_value_project_name_desc
+ }
+ end
+
+ def packages_sort_option_title(sort_value)
+ packages_sort_options_hash[sort_value] || sort_title_created_date
+ end
+
+ def packages_sort_direction_button(sort_value)
+ reverse_sort = packages_reverse_sort_order_hash[sort_value]
+ url = package_sort_path(sort: reverse_sort)
+
+ sort_direction_button(url, reverse_sort, sort_value)
+ end
end
SortingHelper.prepend_if_ee('::EE::SortingHelper')
diff --git a/app/helpers/timeboxes_helper.rb b/app/helpers/timeboxes_helper.rb
index 0bffdba7349..34919f994ee 100644
--- a/app/helpers/timeboxes_helper.rb
+++ b/app/helpers/timeboxes_helper.rb
@@ -40,7 +40,7 @@ module TimeboxesHelper
opts = { milestone_title: milestone.title, state: state }
if @project
- polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
+ polymorphic_path([@project, type], opts)
elsif @group
polymorphic_url([type, @group], opts)
else
@@ -155,7 +155,7 @@ module TimeboxesHelper
opened = milestone.opened_issues_count
closed = milestone.closed_issues_count
- return _("Issues") if total.zero?
+ return _("Issues") if total == 0
content = []
@@ -187,7 +187,7 @@ module TimeboxesHelper
def milestone_releases_tooltip_text(milestone)
count = milestone.releases.count
- return _("Releases") if count.zero?
+ return _("Releases") if count == 0
n_("%{releases} release", "%{releases} releases", count) % { releases: count }
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index b9a6cab07a8..9865f7dfbef 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -163,7 +163,8 @@ module TodosHelper
{ id: '', text: 'Any Type' },
{ id: 'Issue', text: 'Issue' },
{ id: 'MergeRequest', text: 'Merge Request' },
- { id: 'DesignManagement::Design', text: 'Design' }
+ { id: 'DesignManagement::Design', text: 'Design' },
+ { id: 'AlertManagement::Alert', text: 'Alert' }
]
end
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 34c8ce51df0..98159369fb1 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -7,13 +7,15 @@ module UserCalloutsHelper
SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed'
TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
WEBHOOKS_MOVED = 'webhooks_moved'
+ CUSTOMIZE_HOMEPAGE = 'customize_homepage'
def show_admin_integrations_moved?
!user_dismissed?(ADMIN_INTEGRATIONS_MOVED)
end
def show_gke_cluster_integration_callout?(project)
- can?(current_user, :create_cluster, project) &&
+ active_nav_link?(controller: sidebar_operations_paths) &&
+ can?(current_user, :create_cluster, project) &&
!user_dismissed?(GKE_CLUSTER_INTEGRATION)
end
@@ -35,14 +37,14 @@ module UserCalloutsHelper
!user_dismissed?(SUGGEST_POPOVER_DISMISSED)
end
- def show_tabs_feature_highlight?
- current_user && !user_dismissed?(TABS_POSITION_HIGHLIGHT) && !Rails.env.test?
- end
-
def show_webhooks_moved_alert?
!user_dismissed?(WEBHOOKS_MOVED)
end
+ def show_customize_homepage_banner?(customize_homepage)
+ customize_homepage && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
+ end
+
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index cf2d2d178e1..ad33ac66f38 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -80,7 +80,7 @@ module WikiHelper
link_to(wiki_path(wiki, action: :pages, sort: sort, direction: reversed_direction),
type: 'button', class: link_class, title: _('Sort direction')) do
- sprite_icon("sort-#{icon_class}", size: 16)
+ sprite_icon("sort-#{icon_class}")
end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index c327a0bab43..b45755788b8 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -45,6 +45,17 @@ module Emails
end
end
+ def access_token_expired_email(user)
+ return unless user && user.active?
+
+ @user = user
+ @target_url = profile_personal_access_tokens_url
+
+ Gitlab::I18n.with_locale(@user.preferred_language) do
+ mail(to: @user.notification_email, subject: subject(_("Your personal access token has expired")))
+ end
+ end
+
def unknown_sign_in_email(user, ip, time)
@user = user
@ip = ip
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index fb166fb56b7..75581805b49 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -118,6 +118,7 @@ module AlertManagement
end
delegate :iid, to: :issue, prefix: true, allow_nil: true
+ delegate :metrics_dashboard_url, :runbook, :details_url, to: :present
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_status, -> (status) { where(status: status) }
@@ -136,6 +137,7 @@ module AlertManagement
# Descending sort order sorts severity from more critical to less critical.
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) }
+ scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) }
# Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
# Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 9ec407a10a4..91b8bfedcbb 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -42,15 +42,15 @@ class ApplicationRecord < ActiveRecord::Base
limit(count)
end
- def self.safe_find_or_create_by!(*args)
- safe_find_or_create_by(*args).tap do |record|
+ def self.safe_find_or_create_by!(*args, &block)
+ safe_find_or_create_by(*args, &block).tap do |record|
record.validate! unless record.persisted?
end
end
- def self.safe_find_or_create_by(*args)
+ def self.safe_find_or_create_by(*args, &block)
safe_ensure_unique(retries: 1) do
- find_or_create_by(*args)
+ find_or_create_by(*args, &block)
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 25b81aef45f..661b10019ad 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -5,11 +5,14 @@ class ApplicationSetting < ApplicationRecord
include CacheMarkdownField
include TokenAuthenticatable
include ChronicDurationAttribute
+ include IgnorableColumns
+
+ ignore_column :namespace_storage_size_limit, remove_with: '13.5', remove_after: '2020-09-22'
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
- add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
+ add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required }
add_authentication_token_field :health_check_access_token
add_authentication_token_field :static_objects_external_storage_auth_token
@@ -272,6 +275,7 @@ class ApplicationSetting < ApplicationRecord
numericality: { greater_than_or_equal_to: 0 }
validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 }
+ validates :wiki_page_max_content_bytes, numericality: { only_integer: true, greater_than_or_equal_to: 1.kilobytes }
validates :email_restrictions, untrusted_regexp: true
@@ -362,10 +366,6 @@ class ApplicationSetting < ApplicationRecord
length: { maximum: 255 },
allow_blank: true
- validates :namespace_storage_size_limit,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
-
validates :issues_create_limit,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 73554ee8457..8bdb80a65b1 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -37,8 +37,8 @@ module ApplicationSettingImplementation
{
after_sign_up_text: nil,
akismet_enabled: false,
- allow_local_requests_from_web_hooks_and_services: false,
allow_local_requests_from_system_hooks: true,
+ allow_local_requests_from_web_hooks_and_services: false,
asset_proxy_enabled: false,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
commit_email_hostname: default_commit_email_hostname,
@@ -47,10 +47,11 @@ module ApplicationSettingImplementation
container_registry_token_expire_delay: 5,
container_registry_vendor: '',
container_registry_version: '',
+ custom_http_clone_url_root: nil,
default_artifacts_expire_in: '30 days',
+ default_branch_name: nil,
default_branch_protection: Settings.gitlab['default_branch_protection'],
default_ci_config_path: nil,
- default_branch_name: nil,
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_project_creation: Settings.gitlab['default_project_creation'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
@@ -63,9 +64,9 @@ module ApplicationSettingImplementation
dsa_key_restriction: 0,
ecdsa_key_restriction: 0,
ed25519_key_restriction: 0,
- eks_integration_enabled: false,
- eks_account_id: nil,
eks_access_key_id: nil,
+ eks_account_id: nil,
+ eks_integration_enabled: false,
eks_secret_access_key: nil,
email_restrictions_enabled: false,
email_restrictions: nil,
@@ -74,6 +75,9 @@ module ApplicationSettingImplementation
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
gravatar_enabled: Settings.gravatar['enabled'],
+ group_download_export_limit: 1,
+ group_export_limit: 6,
+ group_import_limit: 6,
help_page_hide_commercial_content: false,
help_page_text: nil,
hide_third_party_offers: false,
@@ -83,46 +87,57 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'],
+ instance_statistics_visibility_private: false,
issues_create_limit: 300,
local_markdown_version: 0,
+ login_recaptcha_protection_enabled: false,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
max_import_size: 50,
+ minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
mirror_available: true,
notify_on_unknown_sign_in: true,
outbound_local_requests_whitelist: [],
password_authentication_enabled_for_git: true,
password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'],
performance_bar_allowed_group_id: nil,
- rsa_key_restriction: 0,
plantuml_enabled: false,
plantuml_url: nil,
polling_interval_multiplier: 1,
+ productivity_analytics_start_date: Time.current,
+ project_download_export_limit: 1,
project_export_enabled: true,
+ project_export_limit: 6,
+ project_import_limit: 6,
protected_ci_variables: true,
- push_event_hooks_limit: 3,
+ protected_paths: DEFAULT_PROTECTED_PATHS,
push_event_activities_limit: 3,
+ push_event_hooks_limit: 3,
raw_blob_request_limit: 300,
recaptcha_enabled: false,
- login_recaptcha_protection_enabled: false,
repository_checks_enabled: true,
- repository_storages: ['default'],
repository_storages_weighted: { default: 100 },
+ repository_storages: ['default'],
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
- session_expire_delay: Settings.gitlab['session_expire_delay'],
+ rsa_key_restriction: 0,
send_user_confirmation_email: false,
+ session_expire_delay: Settings.gitlab['session_expire_delay'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
shared_runners_text: nil,
sign_in_text: nil,
signup_enabled: Settings.gitlab['signup_enabled'],
+ snippet_size_limit: 50.megabytes,
+ snowplow_app_id: nil,
+ snowplow_collector_hostname: nil,
+ snowplow_cookie_domain: nil,
+ snowplow_enabled: false,
+ snowplow_iglu_registry_url: nil,
sourcegraph_enabled: false,
- sourcegraph_url: nil,
sourcegraph_public_only: true,
+ sourcegraph_url: nil,
spam_check_endpoint_enabled: false,
spam_check_endpoint_url: nil,
- minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
- namespace_storage_size_limit: 0,
terminal_max_session_time: 0,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_period_in_seconds: 3600,
@@ -130,41 +145,26 @@ module ApplicationSettingImplementation
throttle_authenticated_web_enabled: false,
throttle_authenticated_web_period_in_seconds: 3600,
throttle_authenticated_web_requests_per_period: 7200,
- throttle_unauthenticated_enabled: false,
- throttle_unauthenticated_period_in_seconds: 3600,
- throttle_unauthenticated_requests_per_period: 3600,
+ throttle_incident_management_notification_enabled: false,
+ throttle_incident_management_notification_per_period: 3600,
+ throttle_incident_management_notification_period_in_seconds: 3600,
throttle_protected_paths_enabled: false,
throttle_protected_paths_in_seconds: 10,
throttle_protected_paths_per_period: 60,
- protected_paths: DEFAULT_PROTECTED_PATHS,
- throttle_incident_management_notification_enabled: false,
- throttle_incident_management_notification_period_in_seconds: 3600,
- throttle_incident_management_notification_per_period: 3600,
+ throttle_unauthenticated_enabled: false,
+ throttle_unauthenticated_period_in_seconds: 3600,
+ throttle_unauthenticated_requests_per_period: 3600,
time_tracking_limit_to_hours: false,
two_factor_grace_period: 48,
unique_ips_limit_enabled: false,
unique_ips_limit_per_user: 10,
unique_ips_limit_time_window: 3600,
usage_ping_enabled: Settings.gitlab['usage_ping_enabled'],
- instance_statistics_visibility_private: false,
+ usage_stats_set_by_user_id: nil,
user_default_external: false,
user_default_internal_regex: nil,
user_show_add_ssh_key_message: true,
- usage_stats_set_by_user_id: nil,
- snowplow_collector_hostname: nil,
- snowplow_cookie_domain: nil,
- snowplow_enabled: false,
- snowplow_app_id: nil,
- snowplow_iglu_registry_url: nil,
- custom_http_clone_url_root: nil,
- productivity_analytics_start_date: Time.current,
- snippet_size_limit: 50.megabytes,
- project_import_limit: 6,
- project_export_limit: 6,
- project_download_export_limit: 1,
- group_import_limit: 6,
- group_export_limit: 6,
- group_download_export_limit: 1
+ wiki_page_max_content_bytes: 50.megabytes
}
end
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index 13fc2514f0c..e7cfa30a892 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -5,7 +5,7 @@ class AuditEvent < ApplicationRecord
include IgnorableColumns
include BulkInsertSafe
- PARALLEL_PERSISTENCE_COLUMNS = [:author_name, :entity_path].freeze
+ PARALLEL_PERSISTENCE_COLUMNS = [:author_name, :entity_path, :target_details].freeze
ignore_column :updated_at, remove_with: '13.4', remove_after: '2020-09-22'
@@ -58,6 +58,12 @@ class AuditEvent < ApplicationRecord
end
end
+ def as_json(options = {})
+ super(options).tap do |json|
+ json['ip_address'] = self.ip_address.to_s
+ end
+ end
+
private
def default_author_value
diff --git a/app/models/audit_event_partitioned.rb b/app/models/audit_event_partitioned.rb
new file mode 100644
index 00000000000..672daebd14a
--- /dev/null
+++ b/app/models/audit_event_partitioned.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# This model is not yet intended to be used.
+# It is in a transitioning phase while we are partitioning
+# the table on the database-side.
+# Please refer to https://gitlab.com/groups/gitlab-org/-/epics/3206
+# for details.
+class AuditEventPartitioned < ApplicationRecord
+ include PartitionedTable
+
+ self.table_name = 'audit_events_part_5fc467ac26'
+
+ partitioned_by :created_at, strategy: :monthly
+end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 874bf58530e..8a9db8b45ea 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -6,6 +6,8 @@ class Blob < SimpleDelegator
include BlobLanguageFromGitAttributes
include BlobActiveModel
+ MODE_SYMLINK = '120000' # The STRING 120000 is the git-reported octal filemode for a symlink
+
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 6c90645e997..af4e6bb0494 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -73,8 +73,7 @@ module Ci
return unless has_environment?
strong_memoize(:persisted_environment) do
- deployment&.environment ||
- Environment.find_by(name: expanded_environment_name, project: project)
+ Environment.find_by(name: expanded_environment_name, project: project)
end
end
@@ -351,7 +350,7 @@ module Ci
after_transition any => [:failed] do |build|
next unless build.project
- if build.retry_failure?
+ if build.auto_retry_allowed?
begin
Ci::Build.retry(build, build.user)
rescue Gitlab::Access::AccessDeniedError => ex
@@ -373,6 +372,10 @@ module Ci
end
end
+ def auto_retry_allowed?
+ auto_retry.allowed?
+ end
+
def detailed_status(current_user)
Gitlab::Ci::Status::Build::Factory
.new(self, current_user)
@@ -439,27 +442,6 @@ module Ci
pipeline.builds.retried.where(name: self.name).count
end
- def retry_failure?
- max_allowed_retries = nil
- max_allowed_retries ||= options_retry_max if retry_on_reason_or_always?
- max_allowed_retries ||= DEFAULT_RETRIES.fetch(failure_reason.to_sym, 0)
-
- max_allowed_retries > 0 && retries_count < max_allowed_retries
- end
-
- def options_retry_max
- options_retry[:max]
- end
-
- def options_retry_when
- options_retry.fetch(:when, ['always'])
- end
-
- def retry_on_reason_or_always?
- options_retry_when.include?(failure_reason.to_s) ||
- options_retry_when.include?('always')
- end
-
def any_unmet_prerequisites?
prerequisites.present?
end
@@ -474,8 +456,7 @@ module Ci
strong_memoize(:expanded_environment_name) do
# We're using a persisted expanded environment name in order to avoid
# variable expansion per request.
- if Feature.enabled?(:ci_persisted_expanded_environment_name, project, default_enabled: true) &&
- metadata&.expanded_environment_name.present?
+ if metadata&.expanded_environment_name.present?
metadata.expanded_environment_name
else
ExpandVariables.expand(environment, -> { simple_variables })
@@ -543,8 +524,6 @@ module Ci
end
end
- CI_REGISTRY_USER = 'gitlab-ci-token'
-
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables unless persisted?
@@ -556,7 +535,7 @@ module Ci
.append(key: 'CI_JOB_TOKEN', value: token.to_s, public: false, masked: true)
.append(key: 'CI_BUILD_ID', value: id.to_s)
.append(key: 'CI_BUILD_TOKEN', value: token.to_s, public: false, masked: true)
- .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
+ .append(key: 'CI_REGISTRY_USER', value: ::Gitlab::Auth::CI_JOB_USER)
.append(key: 'CI_REGISTRY_PASSWORD', value: token.to_s, public: false, masked: true)
.append(key: 'CI_REPOSITORY_URL', value: repo_url.to_s, public: false)
.concat(deploy_token_variables)
@@ -615,7 +594,7 @@ module Ci
def repo_url
return unless token
- auth = "gitlab-ci-token:#{token}@"
+ auth = "#{::Gitlab::Auth::CI_JOB_USER}:#{token}@"
project.http_url_to_repo.sub(%r{^https?://}) do |prefix|
prefix + auth
end
@@ -668,6 +647,13 @@ module Ci
!artifacts_expired? && artifacts_file&.exists?
end
+ # This method is similar to #artifacts? but it includes the artifacts
+ # locking mechanics. A new method was created to prevent breaking existing
+ # behavior and avoid introducing N+1s.
+ def available_artifacts?
+ (!artifacts_expired? || pipeline.artifacts_locked?) && job_artifacts_archive&.exists?
+ end
+
def artifacts_metadata?
artifacts? && artifacts_metadata&.exists?
end
@@ -878,8 +864,7 @@ module Ci
end
def multi_build_steps?
- options.dig(:release)&.any? &&
- Gitlab::Ci::Features.release_generation_enabled?
+ options.dig(:release)&.any?
end
def hide_secrets(trace)
@@ -962,6 +947,12 @@ module Ci
private
+ def auto_retry
+ strong_memoize(:auto_retry) do
+ Gitlab::Ci::Build::AutoRetry.new(self)
+ end
+ end
+
def dependencies
strong_memoize(:dependencies) do
Ci::BuildDependencies.new(self)
@@ -1017,19 +1008,6 @@ module Ci
end
end
- # The format of the retry option changed in GitLab 11.5: Before it was
- # integer only, after it is a hash. New builds are created with the new
- # format, but builds created before GitLab 11.5 and saved in database still
- # have the old integer only format. This method returns the retry option
- # normalized as a hash in 11.5+ format.
- def options_retry
- strong_memoize(:options_retry) do
- value = options&.dig(:retry)
- value = value.is_a?(Integer) ? { max: value } : value.to_h
- value.with_indifferent_access
- end
- end
-
def has_expiring_artifacts?
artifacts_expire_at.present? && artifacts_expire_at > Time.current
end
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 0a7a0e0772b..407802baf09 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -75,18 +75,16 @@ module Ci
def append(new_data, offset)
raise ArgumentError, 'New data is missing' unless new_data
- raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
+ raise ArgumentError, 'Offset is out of range' if offset < 0 || offset > size
raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize)
- in_lock(*lock_params) do # Write operation is atomic
- unsafe_set_data!(data.byteslice(0, offset) + new_data)
- end
+ in_lock(*lock_params) { unsafe_append_data!(new_data, offset) }
schedule_to_persist if full?
end
def size
- data&.bytesize.to_i
+ @size ||= current_store.size(self) || data&.bytesize
end
def start_offset
@@ -118,7 +116,7 @@ module Ci
raise FailedToPersistDataError, 'Data is not fulfilled in a bucket'
end
- old_store_class = self.class.get_store_class(data_store)
+ old_store_class = current_store
self.raw_data = nil
self.data_store = new_store
@@ -128,16 +126,33 @@ module Ci
end
def get_data
- self.class.get_store_class(data_store).data(self)&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
- rescue Excon::Error::NotFound
- # If the data store is :fog and the file does not exist in the object storage, this method returns nil.
+ current_store.data(self)&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
end
def unsafe_set_data!(value)
raise ArgumentError, 'New data size exceeds chunk size' if value.bytesize > CHUNK_SIZE
- self.class.get_store_class(data_store).set_data(self, value)
+ current_store.set_data(self, value)
+
@data = value
+ @size = value.bytesize
+
+ save! if changed?
+ end
+
+ def unsafe_append_data!(value, offset)
+ new_size = value.bytesize + offset
+
+ if new_size > CHUNK_SIZE
+ raise ArgumentError, 'New data size exceeds chunk size'
+ end
+
+ current_store.append_data(self, value, offset).then do |stored|
+ raise ArgumentError, 'Trace appended incorrectly' if stored != new_size
+ end
+
+ @data = nil
+ @size = new_size
save! if changed?
end
@@ -156,6 +171,10 @@ module Ci
size == CHUNK_SIZE
end
+ def current_store
+ self.class.get_store_class(data_store)
+ end
+
def lock_params
["trace_write:#{build_id}:chunks:#{chunk_index}",
{ ttl: WRITE_LOCK_TTL,
diff --git a/app/models/ci/build_trace_chunks/database.rb b/app/models/ci/build_trace_chunks/database.rb
index 73cb8abf381..3b8e23510d9 100644
--- a/app/models/ci/build_trace_chunks/database.rb
+++ b/app/models/ci/build_trace_chunks/database.rb
@@ -19,8 +19,22 @@ module Ci
model.raw_data
end
- def set_data(model, data)
- model.raw_data = data
+ def set_data(model, new_data)
+ model.raw_data = new_data
+ end
+
+ def append_data(model, new_data, offset)
+ if offset > 0
+ truncated_data = data(model).to_s.byteslice(0, offset)
+ new_data = truncated_data + new_data
+ end
+
+ model.raw_data = new_data
+ model.raw_data.to_s.bytesize
+ end
+
+ def size(model)
+ data(model).to_s.bytesize
end
def delete_data(model)
diff --git a/app/models/ci/build_trace_chunks/fog.rb b/app/models/ci/build_trace_chunks/fog.rb
index a849bd08427..b1e9fd1faeb 100644
--- a/app/models/ci/build_trace_chunks/fog.rb
+++ b/app/models/ci/build_trace_chunks/fog.rb
@@ -9,10 +9,26 @@ module Ci
def data(model)
connection.get_object(bucket_name, key(model))[:body]
+ rescue Excon::Error::NotFound
+ # If the object does not exist in the object storage, this method returns nil.
end
- def set_data(model, data)
- connection.put_object(bucket_name, key(model), data)
+ def set_data(model, new_data)
+ connection.put_object(bucket_name, key(model), new_data)
+ end
+
+ def append_data(model, new_data, offset)
+ if offset > 0
+ truncated_data = data(model).to_s.byteslice(0, offset)
+ new_data = truncated_data + new_data
+ end
+
+ set_data(model, new_data)
+ new_data.bytesize
+ end
+
+ def size(model)
+ data(model).to_s.bytesize
end
def delete_data(model)
diff --git a/app/models/ci/build_trace_chunks/redis.rb b/app/models/ci/build_trace_chunks/redis.rb
index c3864f78b01..0ae563f6ce8 100644
--- a/app/models/ci/build_trace_chunks/redis.rb
+++ b/app/models/ci/build_trace_chunks/redis.rb
@@ -4,6 +4,32 @@ module Ci
module BuildTraceChunks
class Redis
CHUNK_REDIS_TTL = 1.week
+ LUA_APPEND_CHUNK = <<~EOS.freeze
+ local key, new_data, offset = KEYS[1], ARGV[1], ARGV[2]
+ local length = new_data:len()
+ local expire = #{CHUNK_REDIS_TTL.seconds}
+ local current_size = redis.call("strlen", key)
+ offset = tonumber(offset)
+
+ if offset == 0 then
+ -- overwrite everything
+ redis.call("set", key, new_data, "ex", expire)
+ return redis.call("strlen", key)
+ elseif offset > current_size then
+ -- offset range violation
+ return -1
+ elseif offset + length >= current_size then
+ -- efficiently append or overwrite and append
+ redis.call("expire", key, expire)
+ return redis.call("setrange", key, offset, new_data)
+ else
+ -- append and truncate
+ local current_data = redis.call("get", key)
+ new_data = current_data:sub(1, offset) .. new_data
+ redis.call("set", key, new_data, "ex", expire)
+ return redis.call("strlen", key)
+ end
+ EOS
def available?
true
@@ -21,6 +47,18 @@ module Ci
end
end
+ def append_data(model, new_data, offset)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.eval(LUA_APPEND_CHUNK, keys: [key(model)], argv: [new_data, offset])
+ end
+ end
+
+ def size(model)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.strlen(key(model))
+ end
+ end
+
def delete_data(model)
delete_keys([[model.build_id, model.chunk_index]])
end
diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb
index 779c6c0396f..f0c035635b9 100644
--- a/app/models/ci/group.rb
+++ b/app/models/ci/group.rb
@@ -24,15 +24,9 @@ module Ci
def status
strong_memoize(:status) do
- if ::Gitlab::Ci::Features.composite_status?(project)
- Gitlab::Ci::Status::Composite
- .new(@jobs)
- .status
- else
- CommitStatus
- .where(id: @jobs)
- .legacy_status
- end
+ Gitlab::Ci::Status::Composite
+ .new(@jobs)
+ .status
end
end
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index 628749b32cb..e083caa8751 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -14,12 +14,14 @@ module Ci
alias_attribute :secret_value, :value
validates :key, uniqueness: {
- message: "(%{value}) has already been taken"
+ message: -> (object, data) { _("(%{value}) has already been taken") }
}
- validates :encrypted_value, length: {
- maximum: 1024,
- too_long: 'The encrypted value of the provided variable exceeds %{count} bytes. Variables over 700 characters risk exceeding the limit.'
+ validates :value, length: {
+ maximum: 10_000,
+ too_long: -> (object, data) do
+ _('The value of the provided variable exceeds the %{count} character limit')
+ end
}
scope :unprotected, -> { where(protected: false) }
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index dbeba1ece31..75c3ce98c95 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -8,6 +8,8 @@ module Ci
include UsageStatistics
include Sortable
include IgnorableColumns
+ include Artifactable
+ include FileStoreMounter
extend Gitlab::Ci::Model
NotSupportedAdapterError = Class.new(StandardError)
@@ -114,7 +116,7 @@ module Ci
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
- mount_uploader :file, JobArtifactUploader
+ mount_file_store_uploader JobArtifactUploader
validates :file_format, presence: true, unless: :trace?, on: :create
validate :validate_supported_file_format!, on: :create
@@ -123,8 +125,6 @@ module Ci
update_project_statistics project_statistics_name: :build_artifacts_size
- after_save :update_file_store, if: :saved_change_to_file?
-
scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) }
scope :with_files_stored_locally, -> { where(file_store: ::JobArtifactUploader::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
@@ -200,12 +200,6 @@ module Ci
load_performance: 25 ## EE-specific
}
- enum file_format: {
- raw: 1,
- zip: 2,
- gzip: 3
- }, _suffix: true
-
# `file_location` indicates where actual files are stored.
# Ideally, actual files should be stored in the same directory, and use the same
# convention to generate its path. However, sometimes we can't do so due to backward-compatibility.
@@ -220,11 +214,6 @@ module Ci
hashed_path: 2
}
- FILE_FORMAT_ADAPTERS = {
- gzip: Gitlab::Ci::Build::Artifacts::Adapters::GzipStream,
- raw: Gitlab::Ci::Build::Artifacts::Adapters::RawStream
- }.freeze
-
def validate_supported_file_format!
return if Feature.disabled?(:drop_license_management_artifact, project, default_enabled: true)
@@ -239,12 +228,6 @@ module Ci
end
end
- def update_file_store
- # The file.object_store is set during `uploader.store!`
- # which happens after object is inserted/updated
- self.update_column(:file_store, file.object_store)
- end
-
def self.associated_file_types_for(file_type)
return unless file_types.include?(file_type)
@@ -284,7 +267,7 @@ module Ci
def expire_in=(value)
self.expire_at =
if value
- ChronicDuration.parse(value)&.seconds&.from_now
+ ::Gitlab::Ci::Build::Artifacts::ExpireInParser.new(value).seconds_from_now
end
end
@@ -303,16 +286,12 @@ module Ci
end
def self.max_artifact_size(type:, project:)
- max_size = if Feature.enabled?(:ci_max_artifact_size_per_type, project, default_enabled: false)
- limit_name = "#{PLAN_LIMIT_PREFIX}#{type}"
-
- project.actual_limits.limit_for(
- limit_name,
- alternate_limit: -> { project.closest_setting(:max_artifacts_size) }
- )
- else
- project.closest_setting(:max_artifacts_size)
- end
+ limit_name = "#{PLAN_LIMIT_PREFIX}#{type}"
+
+ max_size = project.actual_limits.limit_for(
+ limit_name,
+ alternate_limit: -> { project.closest_setting(:max_artifacts_size) }
+ )
max_size&.megabytes.to_i
end
diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb
index 250306e2be4..df4368eccd5 100644
--- a/app/models/ci/legacy_stage.rb
+++ b/app/models/ci/legacy_stage.rb
@@ -32,7 +32,7 @@ module Ci
end
def status
- @status ||= statuses.latest.slow_composite_status(project: project)
+ @status ||= statuses.latest.composite_status
end
def detailed_status(current_user)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d4b439d648f..7762328d274 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -57,12 +57,12 @@ module Ci
# the merge request's latest commit.
has_many :merge_requests_as_head_pipeline, foreign_key: "head_pipeline_id", class_name: 'MergeRequest'
- has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
+ has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
has_many :failed_builds, -> { latest.failed }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
- has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
+ has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
- has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
- has_many :scheduled_actions, -> { latest.scheduled_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
+ has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
+ has_many :scheduled_actions, -> { latest.scheduled_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
@@ -83,6 +83,7 @@ module Ci
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
accepts_nested_attributes_for :variables, reject_if: :persisted?
@@ -249,14 +250,6 @@ module Ci
pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
end
-
- after_transition any => [:success] do |pipeline|
- next unless Gitlab::Ci::Features.keep_latest_artifacts_for_ref_enabled?(pipeline.project)
-
- pipeline.run_after_commit do
- Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(pipeline.id)
- end
- end
end
scope :internal, -> { where(source: internal_sources) }
@@ -416,7 +409,7 @@ module Ci
def legacy_stage(name)
stage = Ci::LegacyStage.new(self, name: name)
- stage unless stage.statuses_count.zero?
+ stage unless stage.statuses_count == 0
end
def ref_exists?
@@ -425,40 +418,6 @@ module Ci
false
end
- def ordered_stages
- if ::Gitlab::Ci::Features.atomic_processing?(project)
- # The `Ci::Stage` contains all up-to date data
- # as atomic processing updates all data in-bulk
- stages
- elsif complete?
- # The `Ci::Stage` contains up-to date data only for `completed` pipelines
- # this is due to asynchronous processing of pipeline, and stages possibly
- # not updated inline with processing of pipeline
- stages
- else
- # In other cases, we need to calculate stages dynamically
- legacy_stages
- end
- end
-
- def legacy_stages_using_sql
- # TODO, this needs refactoring, see gitlab-foss#26481.
- stages_query = statuses
- .group('stage').select(:stage).order('max(stage_idx)')
-
- status_sql = statuses.latest.where('stage=sg.stage').legacy_status_sql
-
- warnings_sql = statuses.latest.select('COUNT(*)')
- .where('stage=sg.stage').failed_but_allowed.to_sql
-
- stages_with_statuses = CommitStatus.from(stages_query, :sg)
- .pluck('sg.stage', Arel.sql(status_sql), Arel.sql("(#{warnings_sql})"))
-
- stages_with_statuses.map do |stage|
- Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
- end
- end
-
def legacy_stages_using_composite_status
stages = latest_statuses_ordered_by_stage.group_by(&:stage)
@@ -477,12 +436,9 @@ module Ci
triggered_pipelines.preload(:source_job)
end
+ # TODO: Remove usage of this method in templates
def legacy_stages
- if ::Gitlab::Ci::Features.composite_status?(project)
- legacy_stages_using_composite_status
- else
- legacy_stages_using_sql
- end
+ legacy_stages_using_composite_status
end
def valid_commit_sha
@@ -665,7 +621,7 @@ module Ci
end
def has_warnings?
- number_of_warnings.positive?
+ number_of_warnings > 0
end
def number_of_warnings
@@ -755,10 +711,6 @@ module Ci
end
end
- def update_legacy_status
- set_status(latest_builds_status.to_s)
- end
-
def protected_ref?
strong_memoize(:protected_ref) { project.protected_for?(git_ref) }
end
@@ -828,7 +780,7 @@ module Ci
return unless started_at
seconds = (started_at - created_at).to_i
- seconds unless seconds.zero?
+ seconds unless seconds == 0
end
def update_duration
@@ -922,12 +874,6 @@ module Ci
end
end
- def test_reports_count
- Rails.cache.fetch(['project', project.id, 'pipeline', id, 'test_reports_count'], force: false) do
- test_reports.total_count
- end
- end
-
def accessibility_reports
Gitlab::Ci::Reports::AccessibilityReports.new.tap do |accessibility_reports|
builds.latest.with_reports(Ci::JobArtifact.accessibility_reports).each do |build|
@@ -1061,10 +1007,6 @@ module Ci
@persistent_ref ||= PersistentRef.new(pipeline: self)
end
- def find_successful_build_ids_by_names(names)
- statuses.latest.success.where(name: names).pluck(:id)
- end
-
def cacheable?
Ci::PipelineEnums.ci_config_sources.key?(config_source.to_sym)
end
@@ -1084,8 +1026,6 @@ module Ci
end
def ensure_ci_ref!
- return unless Gitlab::Ci::Features.pipeline_fixed_notifications?
-
self.ci_ref = Ci::Ref.ensure_for(self)
end
@@ -1123,12 +1063,6 @@ module Ci
end
end
- def latest_builds_status
- return 'failed' unless yaml_errors.blank?
-
- statuses.latest.slow_composite_status(project: project) || 'skipped'
- end
-
def keep_around_commits
return unless project
diff --git a/app/models/ci/pipeline_artifact.rb b/app/models/ci/pipeline_artifact.rb
new file mode 100644
index 00000000000..e7f51977ccd
--- /dev/null
+++ b/app/models/ci/pipeline_artifact.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+# This class is being used to persist additional artifacts after a pipeline completes, which is a great place to cache a computed result in object storage
+
+module Ci
+ class PipelineArtifact < ApplicationRecord
+ extend Gitlab::Ci::Model
+ include Artifactable
+ include FileStoreMounter
+
+ FILE_STORE_SUPPORTED = [
+ ObjectStorage::Store::LOCAL,
+ ObjectStorage::Store::REMOTE
+ ].freeze
+
+ FILE_SIZE_LIMIT = 10.megabytes.freeze
+
+ belongs_to :project, class_name: "Project", inverse_of: :pipeline_artifacts
+ belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :pipeline_artifacts
+
+ validates :pipeline, :project, :file_format, :file, presence: true
+ validates :file_store, presence: true, inclusion: { in: FILE_STORE_SUPPORTED }
+ validates :size, presence: true, numericality: { less_than_or_equal_to: FILE_SIZE_LIMIT }
+ validates :file_type, presence: true
+
+ mount_file_store_uploader Ci::PipelineArtifactUploader
+ before_save :set_size, if: :file_changed?
+
+ enum file_type: {
+ code_coverage: 1
+ }
+
+ def set_size
+ self.size = file.size
+ end
+ end
+end
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index 352dc56aac7..9d108ff0fa4 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -63,6 +63,10 @@ module Ci
def self.ci_config_sources_values
ci_config_sources.values
end
+
+ def self.non_ci_config_source_values
+ config_sources.values - ci_config_sources.values
+ end
end
end
diff --git a/app/models/ci/ref.rb b/app/models/ci/ref.rb
index 29b44575d65..3d8823728e7 100644
--- a/app/models/ci/ref.rb
+++ b/app/models/ci/ref.rb
@@ -3,6 +3,7 @@
module Ci
class Ref < ApplicationRecord
extend Gitlab::Ci::Model
+ include AfterCommitQueue
include Gitlab::OptimisticLocking
FAILING_STATUSES = %w[failed broken still_failing].freeze
@@ -15,6 +16,7 @@ module Ci
transition unknown: :success
transition fixed: :success
transition %i[failed broken still_failing] => :fixed
+ transition success: same
end
event :do_fail do
@@ -29,6 +31,14 @@ module Ci
state :fixed, value: 3
state :broken, value: 4
state :still_failing, value: 5
+
+ after_transition any => [:fixed, :success] do |ci_ref|
+ next unless ::Gitlab::Ci::Features.keep_latest_artifacts_for_ref_enabled?(ci_ref.project)
+
+ ci_ref.run_after_commit do
+ Ci::PipelineSuccessUnlockArtifactsWorker.perform_async(ci_ref.last_finished_pipeline_id)
+ end
+ end
end
class << self
@@ -47,8 +57,6 @@ module Ci
end
def update_status_by!(pipeline)
- return unless Gitlab::Ci::Features.pipeline_fixed_notifications?
-
retry_lock(self) do
next unless last_finished_pipeline_id == pipeline.id
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 1cd6c64841b..00ee45740bd 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -10,7 +10,7 @@ module Ci
include TokenAuthenticatable
include IgnorableColumns
- add_authentication_token_field :token, encrypted: -> { Feature.enabled?(:ci_runners_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
+ add_authentication_token_field :token, encrypted: -> { Feature.enabled?(:ci_runners_tokens_optional_encryption) ? :optional : :required }
enum access_level: {
not_protected: 0,
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 41215601704..cc6bd1870b9 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -113,7 +113,7 @@ module Ci
end
def has_warnings?
- number_of_warnings.positive?
+ number_of_warnings > 0
end
def number_of_warnings
@@ -138,7 +138,7 @@ module Ci
end
def latest_stage_status
- statuses.latest.slow_composite_status(project: project) || 'skipped'
+ statuses.latest.composite_status || 'skipped'
end
end
end
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
new file mode 100644
index 00000000000..c21759a3c3b
--- /dev/null
+++ b/app/models/clusters/agent.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Clusters
+ class Agent < ApplicationRecord
+ self.table_name = 'cluster_agents'
+
+ belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
+
+ has_many :agent_tokens, class_name: 'Clusters::AgentToken'
+
+ validates :name,
+ presence: true,
+ length: { maximum: 63 },
+ uniqueness: { scope: :project_id },
+ format: {
+ with: Gitlab::Regex.cluster_agent_name_regex,
+ message: Gitlab::Regex.cluster_agent_name_regex_message
+ }
+ end
+end
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
new file mode 100644
index 00000000000..e9f1ee4e033
--- /dev/null
+++ b/app/models/clusters/agent_token.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Clusters
+ class AgentToken < ApplicationRecord
+ include TokenAuthenticatable
+ add_authentication_token_field :token, encrypted: :required
+
+ self.table_name = 'cluster_agent_tokens'
+
+ belongs_to :agent, class_name: 'Clusters::Agent'
+
+ before_save :ensure_token
+ end
+end
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 53c90fa56d5..1efa44c39c5 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -38,8 +38,7 @@ module Clusters
chart: chart,
files: files.merge(cluster_issuer_file),
preinstall: pre_install_script,
- postinstall: post_install_script,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ postinstall: post_install_script
)
end
@@ -48,8 +47,7 @@ module Clusters
name: 'certmanager',
rbac: cluster.platform_kubernetes_rbac?,
files: files,
- postdelete: post_delete_script,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ postdelete: post_delete_script
)
end
diff --git a/app/models/clusters/applications/crossplane.rb b/app/models/clusters/applications/crossplane.rb
index 2e5a8210b3c..420e56c1742 100644
--- a/app/models/clusters/applications/crossplane.rb
+++ b/app/models/clusters/applications/crossplane.rb
@@ -35,8 +35,7 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
- files: files,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ files: files
)
end
diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb
index 58ac0c1f188..77996748b81 100644
--- a/app/models/clusters/applications/elastic_stack.rb
+++ b/app/models/clusters/applications/elastic_stack.rb
@@ -34,8 +34,7 @@ module Clusters
repository: repository,
files: files,
preinstall: migrate_to_3_script,
- postinstall: post_install_script,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ postinstall: post_install_script
)
end
@@ -44,8 +43,7 @@ module Clusters
name: 'elastic-stack',
rbac: cluster.platform_kubernetes_rbac?,
files: files,
- postdelete: post_delete_script,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ postdelete: post_delete_script
)
end
@@ -121,8 +119,7 @@ module Clusters
Gitlab::Kubernetes::Helm::DeleteCommand.new(
name: 'elastic-stack',
rbac: cluster.platform_kubernetes_rbac?,
- files: files,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ files: files
).delete_command,
Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack", "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE)
]
diff --git a/app/models/clusters/applications/fluentd.rb b/app/models/clusters/applications/fluentd.rb
index 1bcd39618f6..3fd6e870edc 100644
--- a/app/models/clusters/applications/fluentd.rb
+++ b/app/models/clusters/applications/fluentd.rb
@@ -32,8 +32,7 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
- files: files,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ files: files
)
end
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index 226a9c26db0..4a1bcac4bb7 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -52,8 +52,7 @@ module Clusters
Gitlab::Kubernetes::Helm::InitCommand.new(
name: name,
files: files,
- rbac: cluster.platform_kubernetes_rbac?,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ rbac: cluster.platform_kubernetes_rbac?
)
end
@@ -61,8 +60,7 @@ module Clusters
Gitlab::Kubernetes::Helm::ResetCommand.new(
name: name,
files: files,
- rbac: cluster.platform_kubernetes_rbac?,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ rbac: cluster.platform_kubernetes_rbac?
)
end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index a44450ec7a9..1d08f38a2f1 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Ingress < ApplicationRecord
- VERSION = '1.29.7'
+ VERSION = '1.40.2'
INGRESS_CONTAINER_NAME = 'nginx-ingress-controller'
MODSECURITY_LOG_CONTAINER_NAME = 'modsecurity-log'
MODSECURITY_MODE_LOGGING = "DetectionOnly"
@@ -63,8 +63,7 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
- files: files,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ files: files
)
end
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index b737f0f962f..056ea355de6 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -45,8 +45,7 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
- repository: repository,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ repository: repository
)
end
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index b55fc3c45fc..3047da12dd9 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -77,8 +77,7 @@ module Clusters
chart: chart,
files: files,
repository: REPOSITORY,
- postinstall: install_knative_metrics,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ postinstall: install_knative_metrics
)
end
@@ -100,8 +99,7 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
files: files,
predelete: delete_knative_services_and_metrics,
- postdelete: delete_knative_istio_leftovers,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ postdelete: delete_knative_istio_leftovers
)
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 101d782db3a..216bbbc1c5a 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -69,8 +69,7 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
- postinstall: install_knative_metrics,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ postinstall: install_knative_metrics
)
end
@@ -80,8 +79,7 @@ module Clusters
version: version,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
- files: files_with_replaced_values(values),
- local_tiller_enabled: cluster.local_tiller_enabled?
+ files: files_with_replaced_values(values)
)
end
@@ -90,8 +88,7 @@ module Clusters
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files,
- predelete: delete_knative_istio_metrics,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ predelete: delete_knative_istio_metrics
)
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 3f0b4edde35..c041f605e6c 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.18.2'
+ VERSION = '0.19.2'
self.table_name = 'clusters_applications_runners'
@@ -36,8 +36,7 @@ module Clusters
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
files: files,
- repository: repository,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ repository: repository
)
end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 7641b6d2a4b..63aebdf1bdb 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -218,6 +218,24 @@ module Clusters
provider&.status_name || connection_status.presence || :created
end
+ def connection_error
+ with_reactive_cache do |data|
+ data[:connection_error]
+ end
+ end
+
+ def node_connection_error
+ with_reactive_cache do |data|
+ data[:node_connection_error]
+ end
+ end
+
+ def metrics_connection_error
+ with_reactive_cache do |data|
+ data[:metrics_connection_error]
+ end
+ end
+
def connection_status
with_reactive_cache do |data|
data[:connection_status]
@@ -233,9 +251,7 @@ module Clusters
def calculate_reactive_cache
return unless enabled?
- gitlab_kubernetes_nodes = Gitlab::Kubernetes::Node.new(self)
-
- { connection_status: retrieve_connection_status, nodes: gitlab_kubernetes_nodes.all.presence }
+ connection_data.merge(Gitlab::Kubernetes::Node.new(self).all)
end
def persisted_applications
@@ -341,10 +357,6 @@ module Clusters
end
end
- def local_tiller_enabled?
- Feature.enabled?(:managed_apps_local_tiller, clusterable, default_enabled: true)
- end
-
def prometheus_adapter
application_prometheus
end
@@ -395,9 +407,10 @@ module Clusters
@instance_domain ||= Gitlab::CurrentSettings.auto_devops_domain
end
- def retrieve_connection_status
+ def connection_data
result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.core_client.discover }
- result[:status]
+
+ { connection_status: result[:status], connection_error: result[:connection_error] }.compact
end
# To keep backward compatibility with AUTO_DEVOPS_DOMAIN
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
index c1f63758906..760576ea1eb 100644
--- a/app/models/clusters/concerns/application_core.rb
+++ b/app/models/clusters/concerns/application_core.rb
@@ -15,7 +15,7 @@ module Clusters
def set_initial_status
return unless not_installable?
- self.status = status_states[:installable] if cluster&.application_helm_available? || cluster&.local_tiller_enabled?
+ self.status = status_states[:installable]
end
def can_uninstall?
diff --git a/app/models/clusters/concerns/application_data.rb b/app/models/clusters/concerns/application_data.rb
index ade27e69642..22e597e9747 100644
--- a/app/models/clusters/concerns/application_data.rb
+++ b/app/models/clusters/concerns/application_data.rb
@@ -7,8 +7,7 @@ module Clusters
Gitlab::Kubernetes::Helm::DeleteCommand.new(
name: name,
rbac: cluster.platform_kubernetes_rbac?,
- files: files,
- local_tiller_enabled: cluster.local_tiller_enabled?
+ files: files
)
end
@@ -21,23 +20,11 @@ module Clusters
end
def files
- @files ||= begin
- files = { 'values.yaml': values }
-
- files.merge!(certificate_files) if use_tiller_ssl?
-
- files
- end
+ @files ||= { 'values.yaml': values }
end
private
- def use_tiller_ssl?
- return false if cluster.local_tiller_enabled?
-
- cluster.application_helm.has_ssl?
- end
-
def certificate_files
{
'ca.pem': ca_cert,
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 86d74ed7b1c..95ac95448dd 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -79,7 +79,7 @@ module Clusters
transition [:scheduled] => :uninstalling
end
- before_transition any => [:scheduled] do |application, _|
+ before_transition any => [:scheduled, :installed, :uninstalled] do |application, _|
application.status_reason = nil
end
@@ -97,24 +97,6 @@ module Clusters
application.status_reason = status_reason if status_reason
end
- before_transition any => [:installed, :updated] do |application, transition|
- unless application.cluster.local_tiller_enabled? || application.is_a?(Clusters::Applications::Helm)
- if transition.event == :make_externally_installed
- # If an application is externally installed
- # We assume the helm application is externally installed too
- helm = application.cluster.application_helm || application.cluster.build_application_helm
-
- helm.make_externally_installed!
- else
- # When installing any application we are also performing an update
- # of tiller (see Gitlab::Kubernetes::Helm::ClientCommand) so
- # therefore we need to reflect that in the database.
-
- application.cluster.application_helm.update!(version: Gitlab::Kubernetes::Helm::HELM_VERSION)
- end
- end
- end
-
after_transition any => [:uninstalling], :use_transactions => false do |application, _|
application.prepare_uninstall
end
diff --git a/app/models/clusters/providers/aws.rb b/app/models/clusters/providers/aws.rb
index faf587fb83d..86869361ed8 100644
--- a/app/models/clusters/providers/aws.rb
+++ b/app/models/clusters/providers/aws.rb
@@ -5,6 +5,9 @@ module Clusters
class Aws < ApplicationRecord
include Gitlab::Utils::StrongMemoize
include Clusters::Concerns::ProviderStatus
+ include IgnorableColumns
+
+ ignore_column :created_by_user_id, remove_with: '13.4', remove_after: '2020-08-22'
self.table_name = 'cluster_providers_aws'
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 53bcdf8165f..4f18ece9e50 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -21,7 +21,6 @@ class Commit
participant :committer
participant :notes_with_associations
- attr_accessor :author
attr_accessor :redacted_description_html
attr_accessor :redacted_title_html
attr_accessor :redacted_full_title_html
diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb
index b8653f47392..07c49ed48e6 100644
--- a/app/models/commit_collection.rb
+++ b/app/models/commit_collection.rb
@@ -47,7 +47,10 @@ class CommitCollection
pipelines = project.ci_pipelines.latest_pipeline_per_commit(map(&:id), ref)
each do |commit|
- commit.set_latest_pipeline_for_ref(ref, pipelines[commit.id])
+ pipeline = pipelines[commit.id]
+ pipeline&.number_of_warnings # preload number of warnings
+
+ commit.set_latest_pipeline_for_ref(ref, pipeline)
end
self
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index c85292feb25..8aba74bedbc 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -100,9 +100,7 @@ class CommitStatus < ApplicationRecord
# will not be refreshed to pick the change
self.processed_will_change!
- if !::Gitlab::Ci::Features.atomic_processing?(project)
- self.processed = nil
- elsif latest?
+ if latest?
self.processed = false # force refresh of all dependent ones
elsif retried?
self.processed = true # retried are considered to be already processed
@@ -164,8 +162,7 @@ class CommitStatus < ApplicationRecord
next unless commit_status.project
commit_status.run_after_commit do
- schedule_stage_and_pipeline_update
-
+ PipelineProcessWorker.perform_async(pipeline_id)
ExpireJobCacheWorker.perform_async(id)
end
end
@@ -186,14 +183,6 @@ class CommitStatus < ApplicationRecord
select(:name)
end
- def self.status_for_prior_stages(index, project:)
- before_stage(index).latest.slow_composite_status(project: project) || 'success'
- end
-
- def self.status_for_names(names, project:)
- where(name: names).latest.slow_composite_status(project: project) || 'success'
- end
-
def self.update_as_processed!
# Marks items as processed
# we do not increase `lock_version`, as we are the one
@@ -286,21 +275,6 @@ class CommitStatus < ApplicationRecord
def unrecoverable_failure?
script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure?
end
-
- def schedule_stage_and_pipeline_update
- if ::Gitlab::Ci::Features.atomic_processing?(project)
- # Atomic Processing requires only single Worker
- PipelineProcessWorker.perform_async(pipeline_id, [id])
- else
- if complete? || manual?
- PipelineProcessWorker.perform_async(pipeline_id, [id])
- else
- PipelineUpdateWorker.perform_async(pipeline_id)
- end
-
- StageUpdateWorker.perform_async(stage_id)
- end
- end
end
CommitStatus.prepend_if_ee('::EE::CommitStatus')
diff --git a/app/models/commit_status_enums.rb b/app/models/commit_status_enums.rb
index caebff91022..ad90929b8fa 100644
--- a/app/models/commit_status_enums.rb
+++ b/app/models/commit_status_enums.rb
@@ -23,7 +23,8 @@ module CommitStatusEnums
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
bridge_pipeline_is_child_pipeline: 1_006,
- downstream_pipeline_creation_failed: 1_007
+ downstream_pipeline_creation_failed: 1_007,
+ secrets_provider_not_found: 1_008
}
end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 60de20c3b31..0dd55ab67b5 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -3,6 +3,17 @@
module Avatarable
extend ActiveSupport::Concern
+ ALLOWED_IMAGE_SCALER_WIDTHS = [
+ 400,
+ 200,
+ 64,
+ 48,
+ 40,
+ 26,
+ 20,
+ 16
+ ].freeze
+
included do
prepend ShadowMethods
include ObjectStorage::BackgroundMove
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 04eb4659469..49fc780f372 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -39,6 +39,10 @@ module CacheMarkdownField
context[:markdown_engine] = :common_mark
+ if Feature.enabled?(:personal_snippet_reference_filters, context[:author])
+ context[:user] = self.parent_user
+ end
+
context
end
@@ -132,6 +136,10 @@ module CacheMarkdownField
end
end
+ def parent_user
+ nil
+ end
+
included do
cattr_reader :cached_markdown_fields do
Gitlab::MarkdownCache::FieldData.new
diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb
new file mode 100644
index 00000000000..54fb9021f2f
--- /dev/null
+++ b/app/models/concerns/ci/artifactable.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Ci
+ module Artifactable
+ extend ActiveSupport::Concern
+
+ FILE_FORMAT_ADAPTERS = {
+ gzip: Gitlab::Ci::Build::Artifacts::Adapters::GzipStream,
+ raw: Gitlab::Ci::Build::Artifacts::Adapters::RawStream
+ }.freeze
+
+ included do
+ enum file_format: {
+ raw: 1,
+ zip: 2,
+ gzip: 3
+ }, _suffix: true
+ end
+ end
+end
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index 10df5e1a8dc..c8b55e7b39f 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -9,7 +9,7 @@ module Ci
##
# Variables in the environment name scope.
#
- def scoped_variables(environment: expanded_environment_name)
+ def scoped_variables(environment: expanded_environment_name, dependencies: true)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables)
variables.concat(project.predefined_variables)
@@ -18,7 +18,7 @@ module Ci
variables.concat(deployment_variables(environment: environment))
variables.concat(yaml_variables)
variables.concat(user_variables)
- variables.concat(dependency_variables)
+ variables.concat(dependency_variables) if dependencies
variables.concat(secret_instance_variables)
variables.concat(secret_group_variables)
variables.concat(secret_project_variables(environment: environment))
@@ -45,6 +45,12 @@ module Ci
end
end
+ def simple_variables_without_dependencies
+ strong_memoize(:variables_without_dependencies) do
+ scoped_variables(environment: nil, dependencies: false).to_runner_variables
+ end
+ end
+
def user_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
break variables if user.blank?
@@ -64,7 +70,7 @@ module Ci
variables.append(key: 'CI_PIPELINE_TRIGGERED', value: 'true') if trigger_request
variables.append(key: 'CI_NODE_INDEX', value: self.options[:instance].to_s) if self.options&.include?(:instance)
- variables.append(key: 'CI_NODE_TOTAL', value: (self.options&.dig(:parallel) || 1).to_s)
+ variables.append(key: 'CI_NODE_TOTAL', value: ci_node_total_value.to_s)
# legacy variables
variables.append(key: 'CI_BUILD_NAME', value: name)
@@ -96,5 +102,13 @@ module Ci
def secret_project_variables(environment: persisted_environment)
project.ci_variables_for(ref: git_ref, environment: environment)
end
+
+ private
+
+ def ci_node_total_value
+ parallel = self.options&.dig(:parallel)
+ parallel = parallel.dig(:total) if parallel.is_a?(Hash)
+ parallel || 1
+ end
end
end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index c52807ec501..1cc2e8a51e3 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -20,60 +20,10 @@ module Ci
UnknownStatusError = Class.new(StandardError)
class_methods do
- def legacy_status_sql
- scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all
- scope_warnings = respond_to?(:failed_but_allowed) ? failed_but_allowed : none
-
- builds = scope_relevant.select('count(*)').to_sql
- created = scope_relevant.created.select('count(*)').to_sql
- success = scope_relevant.success.select('count(*)').to_sql
- manual = scope_relevant.manual.select('count(*)').to_sql
- scheduled = scope_relevant.scheduled.select('count(*)').to_sql
- preparing = scope_relevant.preparing.select('count(*)').to_sql
- waiting_for_resource = scope_relevant.waiting_for_resource.select('count(*)').to_sql
- pending = scope_relevant.pending.select('count(*)').to_sql
- running = scope_relevant.running.select('count(*)').to_sql
- skipped = scope_relevant.skipped.select('count(*)').to_sql
- canceled = scope_relevant.canceled.select('count(*)').to_sql
- warnings = scope_warnings.select('count(*) > 0').to_sql.presence || 'false'
-
- Arel.sql(
- "(CASE
- WHEN (#{builds})=(#{skipped}) AND (#{warnings}) THEN 'success'
- WHEN (#{builds})=(#{skipped}) THEN 'skipped'
- WHEN (#{builds})=(#{success}) THEN 'success'
- WHEN (#{builds})=(#{created}) THEN 'created'
- WHEN (#{builds})=(#{preparing}) THEN 'preparing'
- WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success'
- WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
- WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
- WHEN (#{running})+(#{pending})>0 THEN 'running'
- WHEN (#{waiting_for_resource})>0 THEN 'waiting_for_resource'
- WHEN (#{manual})>0 THEN 'manual'
- WHEN (#{scheduled})>0 THEN 'scheduled'
- WHEN (#{preparing})>0 THEN 'preparing'
- WHEN (#{created})>0 THEN 'running'
- ELSE 'failed'
- END)"
- )
- end
-
- def legacy_status
- all.pluck(legacy_status_sql).first
- end
-
- # This method should not be used.
- # This method performs expensive calculation of status:
- # 1. By plucking all related objects,
- # 2. Or executes expensive SQL query
- def slow_composite_status(project:)
- if ::Gitlab::Ci::Features.composite_status?(project)
- Gitlab::Ci::Status::Composite
- .new(all, with_allow_failure: columns_hash.key?('allow_failure'))
- .status
- else
- legacy_status
- end
+ def composite_status
+ Gitlab::Ci::Status::Composite
+ .new(all, with_allow_failure: columns_hash.key?('allow_failure'))
+ .status
end
def started_at
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
new file mode 100644
index 00000000000..a5c7393e8f7
--- /dev/null
+++ b/app/models/concerns/counter_attribute.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+# Add capabilities to increment a numeric model attribute efficiently by
+# using Redis and flushing the increments asynchronously to the database
+# after a period of time (10 minutes).
+# When an attribute is incremented by a value, the increment is added
+# to a Redis key. Then, FlushCounterIncrementsWorker will execute
+# `flush_increments_to_database!` which removes increments from Redis for a
+# given model attribute and updates the values in the database.
+#
+# @example:
+#
+# class ProjectStatistics
+# include CounterAttribute
+#
+# counter_attribute :commit_count
+# counter_attribute :storage_size
+# end
+#
+# To increment the counter we can use the method:
+# delayed_increment_counter(:commit_count, 3)
+#
+module CounterAttribute
+ extend ActiveSupport::Concern
+ extend AfterCommitQueue
+ include Gitlab::ExclusiveLeaseHelpers
+
+ LUA_STEAL_INCREMENT_SCRIPT = <<~EOS.freeze
+ local increment_key, flushed_key = KEYS[1], KEYS[2]
+ local increment_value = redis.call("get", increment_key) or 0
+ local flushed_value = redis.call("incrby", flushed_key, increment_value)
+ if flushed_value == 0 then
+ redis.call("del", increment_key, flushed_key)
+ else
+ redis.call("del", increment_key)
+ end
+ return flushed_value
+ EOS
+
+ WORKER_DELAY = 10.minutes
+ WORKER_LOCK_TTL = 10.minutes
+
+ class_methods do
+ def counter_attribute(attribute)
+ counter_attributes << attribute
+ end
+
+ def counter_attributes
+ @counter_attributes ||= Set.new
+ end
+ end
+
+ # This method must only be called by FlushCounterIncrementsWorker
+ # because it should run asynchronously and with exclusive lease.
+ # This will
+ # 1. temporarily move the pending increment for a given attribute
+ # to a relative "flushed" Redis key, delete the increment key and return
+ # the value. If new increments are performed at this point, the increment
+ # key is recreated as part of `delayed_increment_counter`.
+ # The "flushed" key is used to ensure that we can keep incrementing
+ # counters in Redis while flushing existing values.
+ # 2. then the value is used to update the counter in the database.
+ # 3. finally the "flushed" key is deleted.
+ def flush_increments_to_database!(attribute)
+ lock_key = counter_lock_key(attribute)
+
+ with_exclusive_lease(lock_key) do
+ increment_key = counter_key(attribute)
+ flushed_key = counter_flushed_key(attribute)
+ increment_value = steal_increments(increment_key, flushed_key)
+
+ next if increment_value == 0
+
+ transaction do
+ unsafe_update_counters(id, attribute => increment_value)
+ redis_state { |redis| redis.del(flushed_key) }
+ end
+ end
+ end
+
+ def delayed_increment_counter(attribute, increment)
+ return if increment == 0
+
+ run_after_commit_or_now do
+ if counter_attribute_enabled?(attribute)
+ redis_state do |redis|
+ redis.incrby(counter_key(attribute), increment)
+ end
+
+ FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute)
+ else
+ legacy_increment!(attribute, increment)
+ end
+ end
+
+ true
+ end
+
+ def counter_key(attribute)
+ "project:{#{project_id}}:counters:#{self.class}:#{id}:#{attribute}"
+ end
+
+ def counter_flushed_key(attribute)
+ counter_key(attribute) + ':flushed'
+ end
+
+ def counter_lock_key(attribute)
+ counter_key(attribute) + ':lock'
+ end
+
+ private
+
+ def counter_attribute_enabled?(attribute)
+ Feature.enabled?(:efficient_counter_attribute, project) &&
+ self.class.counter_attributes.include?(attribute)
+ 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 legacy_increment!(attribute, increment)
+ increment!(attribute, increment)
+ end
+
+ def unsafe_update_counters(id, increments)
+ self.class.update_counters(id, increments)
+ end
+
+ def redis_state(&block)
+ Gitlab::Redis::SharedState.with(&block)
+ end
+
+ def with_exclusive_lease(lock_key)
+ in_lock(lock_key, ttl: WORKER_LOCK_TTL) do
+ yield
+ end
+ rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
+ # a worker is already updating the counters
+ end
+end
diff --git a/app/models/concerns/file_store_mounter.rb b/app/models/concerns/file_store_mounter.rb
new file mode 100644
index 00000000000..9d4463e5297
--- /dev/null
+++ b/app/models/concerns/file_store_mounter.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module FileStoreMounter
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def mount_file_store_uploader(uploader)
+ mount_uploader(:file, uploader)
+
+ after_save :update_file_store, if: :saved_change_to_file?
+ end
+ end
+
+ private
+
+ def update_file_store
+ # The file.object_store is set during `uploader.store!`
+ # which happens after object is inserted/updated
+ self.update_column(:file_store, file.object_store)
+ end
+end
diff --git a/app/models/concerns/has_wiki.rb b/app/models/concerns/has_wiki.rb
index 4dd72216e77..3e7cb940a62 100644
--- a/app/models/concerns/has_wiki.rb
+++ b/app/models/concerns/has_wiki.rb
@@ -17,7 +17,7 @@ module HasWiki
def wiki
strong_memoize(:wiki) do
- Wiki.for_container(self, self.owner)
+ Wiki.for_container(self, self.default_owner)
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 715cbd15d93..dd5aedbb760 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -61,11 +61,13 @@ module Issuable
end
end
+ has_many :note_authors, -> { distinct }, through: :notes, source: :author
+
has_many :label_links, as: :target, dependent: :destroy, inverse_of: :target # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_one :metrics
+ has_one :metrics, inverse_of: model_name.singular.to_sym, autosave: true
delegate :name,
:email,
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index 8f8494a9678..ccb334343ff 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -15,7 +15,7 @@ module Milestoneable
validate :milestone_is_valid
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
- scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
+ scope :any_milestone, -> { where.not(milestone_id: nil) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) }
scope :any_release, -> { joins_milestone_releases }
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 1d89a4497d9..d1f04609693 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -3,11 +3,15 @@
# This module makes it possible to handle items as a list, where the order of items can be easily altered
# Requirements:
#
-# - Only works for ActiveRecord models
-# - relative_position integer field must present on the model
-# - This module uses GROUP BY: the model should have a parent relation, example: project -> issues, project is the parent relation (issues table has a parent_id column)
+# The model must have the following named columns:
+# - id: integer
+# - relative_position: integer
#
-# Setup like this in the body of your class:
+# The model must support a concept of siblings via a child->parent relationship,
+# to enable rebalancing and `GROUP BY` in queries.
+# - example: project -> issues, project is the parent relation (issues table has a parent_id column)
+#
+# Two class methods must be defined when including this concern:
#
# include RelativePositioning
#
@@ -24,53 +28,167 @@
module RelativePositioning
extend ActiveSupport::Concern
- MIN_POSITION = 0
- START_POSITION = Gitlab::Database::MAX_INT_VALUE / 2
+ STEPS = 10
+ IDEAL_DISTANCE = 2**(STEPS - 1) + 1
+
+ MIN_POSITION = Gitlab::Database::MIN_INT_VALUE
+ START_POSITION = 0
MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
- IDEAL_DISTANCE = 500
- class_methods do
- def move_nulls_to_end(objects)
- objects = objects.reject(&:relative_position)
+ MAX_GAP = IDEAL_DISTANCE * 2
+ MIN_GAP = 2
- return if objects.empty?
+ NoSpaceLeft = Class.new(StandardError)
- max_relative_position = objects.first.max_relative_position
+ class_methods do
+ def move_nulls_to_end(objects)
+ move_nulls(objects, at_end: true)
+ end
- self.transaction do
- objects.each do |object|
- relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
- object.relative_position = relative_position
- max_relative_position = relative_position
- object.save(touch: false)
- end
- end
+ def move_nulls_to_start(objects)
+ move_nulls(objects, at_end: false)
end
# This method takes two integer values (positions) and
# calculates the position between them. The range is huge as
- # the maximum integer value is 2147483647. We are incrementing position by IDEAL_DISTANCE * 2 every time
- # when we have enough space. If distance is less than IDEAL_DISTANCE, we are calculating an average number.
+ # the maximum integer value is 2147483647.
+ #
+ # We avoid open ranges by clamping the range to [MIN_POSITION, MAX_POSITION].
+ #
+ # Then we handle one of three cases:
+ # - If the gap is too small, we raise NoSpaceLeft
+ # - If the gap is larger than MAX_GAP, we place the new position at most
+ # IDEAL_DISTANCE from the edge of the gap.
+ # - otherwise we place the new position at the midpoint.
+ #
+ # The new position will always satisfy: pos_before <= midpoint <= pos_after
+ #
+ # As a precondition, the gap between pos_before and pos_after MUST be >= 2.
+ # If the gap is too small, NoSpaceLeft is raised.
+ #
+ # This class method should only be called by instance methods of this module, which
+ # include handling for minimum gap size.
+ #
+ # @raises NoSpaceLeft
+ # @api private
def position_between(pos_before, pos_after)
pos_before ||= MIN_POSITION
pos_after ||= MAX_POSITION
pos_before, pos_after = [pos_before, pos_after].sort
- halfway = (pos_after + pos_before) / 2
- distance_to_halfway = pos_after - halfway
+ gap_width = pos_after - pos_before
+ midpoint = [pos_after - 1, pos_before + (gap_width / 2)].min
- if distance_to_halfway < IDEAL_DISTANCE
- halfway
- else
+ if gap_width < MIN_GAP
+ raise NoSpaceLeft
+ elsif gap_width > MAX_GAP
if pos_before == MIN_POSITION
pos_after - IDEAL_DISTANCE
elsif pos_after == MAX_POSITION
pos_before + IDEAL_DISTANCE
else
- halfway
+ midpoint
+ end
+ else
+ midpoint
+ end
+ end
+
+ private
+
+ # @api private
+ def gap_size(object, gaps:, at_end:, starting_from:)
+ total_width = IDEAL_DISTANCE * gaps
+ size = if at_end && starting_from + total_width >= MAX_POSITION
+ (MAX_POSITION - starting_from) / gaps
+ elsif !at_end && starting_from - total_width <= MIN_POSITION
+ (starting_from - MIN_POSITION) / gaps
+ else
+ IDEAL_DISTANCE
+ end
+
+ # Shift max elements leftwards if there isn't enough space
+ return [size, starting_from] if size >= MIN_GAP
+
+ order = at_end ? :desc : :asc
+ terminus = object
+ .send(:relative_siblings) # rubocop:disable GitlabSecurity/PublicSend
+ .where('relative_position IS NOT NULL')
+ .order(relative_position: order)
+ .first
+
+ if at_end
+ terminus.move_sequence_before(true)
+ max_relative_position = terminus.reset.relative_position
+ [[(MAX_POSITION - max_relative_position) / gaps, IDEAL_DISTANCE].min, max_relative_position]
+ else
+ terminus.move_sequence_after(true)
+ min_relative_position = terminus.reset.relative_position
+ [[(min_relative_position - MIN_POSITION) / gaps, IDEAL_DISTANCE].min, min_relative_position]
+ end
+ end
+
+ # @api private
+ # @param [Array<RelativePositioning>] objects The objects to give positions to. The relative
+ # order will be preserved (i.e. when this method returns,
+ # objects.first.relative_position < objects.last.relative_position)
+ # @param [Boolean] at_end: The placement.
+ # If `true`, then all objects with `null` positions are placed _after_
+ # all siblings with positions. If `false`, all objects with `null`
+ # positions are placed _before_ all siblings with positions.
+ # @returns [Number] The number of moved records.
+ def move_nulls(objects, at_end:)
+ objects = objects.reject(&:relative_position)
+ return 0 if objects.empty?
+
+ representative = objects.first
+ number_of_gaps = objects.size + 1 # 1 at left, one between each, and one at right
+ position = if at_end
+ representative.max_relative_position
+ else
+ representative.min_relative_position
+ end
+
+ position ||= START_POSITION # If there are no positioned siblings, start from START_POSITION
+
+ gap, position = gap_size(representative, gaps: number_of_gaps, at_end: at_end, starting_from: position)
+
+ # Raise if we could not make enough space
+ raise NoSpaceLeft if gap < MIN_GAP
+
+ indexed = objects.each_with_index.to_a
+ starting_from = at_end ? position : position - (gap * number_of_gaps)
+
+ # Some classes are polymorphic, and not all siblings are in the same table.
+ by_model = indexed.group_by { |pair| pair.first.class }
+
+ by_model.each do |model, pairs|
+ model.transaction do
+ pairs.each_slice(100) do |batch|
+ # These are known to be integers, one from the DB, and the other
+ # calculated by us, and thus safe to interpolate
+ values = batch.map do |obj, i|
+ pos = starting_from + gap * (i + 1)
+ obj.relative_position = pos
+ "(#{obj.id}, #{pos})"
+ end.join(', ')
+
+ model.connection.exec_query(<<~SQL, "UPDATE #{model.table_name} positions")
+ WITH cte(cte_id, new_pos) AS (
+ SELECT *
+ FROM (VALUES #{values}) as t (id, pos)
+ )
+ UPDATE #{model.table_name}
+ SET relative_position = cte.new_pos
+ FROM cte
+ WHERE cte_id = id
+ SQL
+ end
end
end
+
+ objects.size
end
end
@@ -82,11 +200,12 @@ module RelativePositioning
calculate_relative_position('MAX', &block)
end
- def prev_relative_position
+ def prev_relative_position(ignoring: nil)
prev_pos = nil
if self.relative_position
prev_pos = max_relative_position do |relation|
+ relation = relation.id_not_in(ignoring.id) if ignoring.present?
relation.where('relative_position < ?', self.relative_position)
end
end
@@ -94,11 +213,12 @@ module RelativePositioning
prev_pos
end
- def next_relative_position
+ def next_relative_position(ignoring: nil)
next_pos = nil
if self.relative_position
next_pos = min_relative_position do |relation|
+ relation = relation.id_not_in(ignoring.id) if ignoring.present?
relation.where('relative_position > ?', self.relative_position)
end
end
@@ -110,24 +230,44 @@ module RelativePositioning
return move_after(before) unless after
return move_before(after) unless before
- # If there is no place to insert an item we need to create one by moving the item
- # before this and all preceding items until there is a gap
before, after = after, before if after.relative_position < before.relative_position
- if (after.relative_position - before.relative_position) < 2
- after.move_sequence_before
- before.reset
+
+ pos_left = before.relative_position
+ pos_right = after.relative_position
+
+ if pos_right - pos_left < MIN_GAP
+ # Not enough room! Make space by shifting all previous elements to the left
+ # if there is enough space, else to the right
+ gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
+
+ if gap.present?
+ after.move_sequence_before(next_gap: gap)
+ pos_left -= optimum_delta_for_gap(gap)
+ else
+ before.move_sequence_after
+ pos_right = after.reset.relative_position
+ end
end
- self.relative_position = self.class.position_between(before.relative_position, after.relative_position)
+ new_position = self.class.position_between(pos_left, pos_right)
+
+ self.relative_position = new_position
end
def move_after(before = self)
pos_before = before.relative_position
- pos_after = before.next_relative_position
+ pos_after = before.next_relative_position(ignoring: self)
+
+ if pos_before == MAX_POSITION || gap_too_small?(pos_after, pos_before)
+ gap = before.send(:find_next_gap_after) # rubocop:disable GitlabSecurity/PublicSend
- if pos_after && (pos_after - pos_before) < 2
- before.move_sequence_after
- pos_after = before.next_relative_position
+ if gap.nil?
+ before.move_sequence_before(true)
+ pos_before = before.reset.relative_position
+ else
+ before.move_sequence_after(next_gap: gap)
+ pos_after += optimum_delta_for_gap(gap)
+ end
end
self.relative_position = self.class.position_between(pos_before, pos_after)
@@ -135,80 +275,186 @@ module RelativePositioning
def move_before(after = self)
pos_after = after.relative_position
- pos_before = after.prev_relative_position
+ pos_before = after.prev_relative_position(ignoring: self)
+
+ if pos_after == MIN_POSITION || gap_too_small?(pos_before, pos_after)
+ gap = after.send(:find_next_gap_before) # rubocop:disable GitlabSecurity/PublicSend
- if pos_before && (pos_after - pos_before) < 2
- after.move_sequence_before
- pos_before = after.prev_relative_position
+ if gap.nil?
+ after.move_sequence_after(true)
+ pos_after = after.reset.relative_position
+ else
+ after.move_sequence_before(next_gap: gap)
+ pos_before -= optimum_delta_for_gap(gap)
+ end
end
self.relative_position = self.class.position_between(pos_before, pos_after)
end
def move_to_end
- self.relative_position = self.class.position_between(max_relative_position || START_POSITION, MAX_POSITION)
+ max_pos = max_relative_position
+
+ if max_pos.nil?
+ self.relative_position = START_POSITION
+ elsif gap_too_small?(max_pos, MAX_POSITION)
+ max = relative_siblings.order(Gitlab::Database.nulls_last_order('relative_position', 'DESC')).first
+ max.move_sequence_before(true)
+ max.reset
+ self.relative_position = self.class.position_between(max.relative_position, MAX_POSITION)
+ else
+ self.relative_position = self.class.position_between(max_pos, MAX_POSITION)
+ end
end
def move_to_start
- self.relative_position = self.class.position_between(min_relative_position || START_POSITION, MIN_POSITION)
+ min_pos = min_relative_position
+
+ if min_pos.nil?
+ self.relative_position = START_POSITION
+ elsif gap_too_small?(min_pos, MIN_POSITION)
+ min = relative_siblings.order(Gitlab::Database.nulls_last_order('relative_position', 'ASC')).first
+ min.move_sequence_after(true)
+ min.reset
+ self.relative_position = self.class.position_between(MIN_POSITION, min.relative_position)
+ else
+ self.relative_position = self.class.position_between(MIN_POSITION, min_pos)
+ end
end
# Moves the sequence before the current item to the middle of the next gap
- # For example, we have 5 11 12 13 14 15 and the current item is 15
- # This moves the sequence 11 12 13 14 to 8 9 10 11
- def move_sequence_before
- next_gap = find_next_gap_before
+ # For example, we have
+ #
+ # 5 . . . . . 11 12 13 14 [15] 16 . 17
+ # -----------
+ #
+ # This moves the sequence [11 12 13 14] to [8 9 10 11], so we have:
+ #
+ # 5 . . 8 9 10 11 . . . [15] 16 . 17
+ # ---------
+ #
+ # Creating a gap to the left of the current item. We can understand this as
+ # dividing the 5 spaces between 5 and 11 into two smaller gaps of 2 and 3.
+ #
+ # If `include_self` is true, the current item will also be moved, creating a
+ # gap to the right of the current item:
+ #
+ # 5 . . 8 9 10 11 [14] . . . 16 . 17
+ # --------------
+ #
+ # As an optimization, the gap can be precalculated and passed to this method.
+ #
+ # @api private
+ # @raises NoSpaceLeft if the sequence cannot be moved
+ def move_sequence_before(include_self = false, next_gap: find_next_gap_before)
+ raise NoSpaceLeft unless next_gap.present?
+
delta = optimum_delta_for_gap(next_gap)
- move_sequence(next_gap[:start], relative_position, -delta)
+ move_sequence(next_gap[:start], relative_position, -delta, include_self)
end
# Moves the sequence after the current item to the middle of the next gap
- # For example, we have 11 12 13 14 15 21 and the current item is 11
- # This moves the sequence 12 13 14 15 to 15 16 17 18
- def move_sequence_after
- next_gap = find_next_gap_after
+ # For example, we have:
+ #
+ # 8 . 10 [11] 12 13 14 15 . . . . . 21
+ # -----------
+ #
+ # This moves the sequence [12 13 14 15] to [15 16 17 18], so we have:
+ #
+ # 8 . 10 [11] . . . 15 16 17 18 . . 21
+ # -----------
+ #
+ # Creating a gap to the right of the current item. We can understand this as
+ # dividing the 5 spaces between 15 and 21 into two smaller gaps of 3 and 2.
+ #
+ # If `include_self` is true, the current item will also be moved, creating a
+ # gap to the left of the current item:
+ #
+ # 8 . 10 . . . [14] 15 16 17 18 . . 21
+ # ----------------
+ #
+ # As an optimization, the gap can be precalculated and passed to this method.
+ #
+ # @api private
+ # @raises NoSpaceLeft if the sequence cannot be moved
+ def move_sequence_after(include_self = false, next_gap: find_next_gap_after)
+ raise NoSpaceLeft unless next_gap.present?
+
delta = optimum_delta_for_gap(next_gap)
- move_sequence(relative_position, next_gap[:start], delta)
+ move_sequence(relative_position, next_gap[:start], delta, include_self)
end
private
- # Supposing that we have a sequence of items: 1 5 11 12 13 and the current item is 13
- # This would return: `{ start: 11, end: 5 }`
+ def gap_too_small?(pos_a, pos_b)
+ return false unless pos_a && pos_b
+
+ (pos_a - pos_b).abs < MIN_GAP
+ end
+
+ # Find the first suitable gap to the left of the current position.
+ #
+ # Satisfies the relations:
+ # - gap[:start] <= relative_position
+ # - abs(gap[:start] - gap[:end]) >= MIN_GAP
+ # - MIN_POSITION <= gap[:start] <= MAX_POSITION
+ # - MIN_POSITION <= gap[:end] <= MAX_POSITION
+ #
+ # Supposing that the current item is 13, and we have a sequence of items:
+ #
+ # 1 . . . 5 . . . . 11 12 [13] 14 . . 17
+ # ^---------^
+ #
+ # Then we return: `{ start: 11, end: 5 }`
+ #
+ # Here start refers to the end of the gap closest to the current item.
def find_next_gap_before
items_with_next_pos = scoped_items
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos')
.where('relative_position <= ?', relative_position)
.order(relative_position: :desc)
- find_next_gap(items_with_next_pos).tap do |gap|
- gap[:end] ||= MIN_POSITION
- end
+ find_next_gap(items_with_next_pos, MIN_POSITION)
end
- # Supposing that we have a sequence of items: 13 14 15 20 24 and the current item is 13
- # This would return: `{ start: 15, end: 20 }`
+ # Find the first suitable gap to the right of the current position.
+ #
+ # Satisfies the relations:
+ # - gap[:start] >= relative_position
+ # - abs(gap[:start] - gap[:end]) >= MIN_GAP
+ # - MIN_POSITION <= gap[:start] <= MAX_POSITION
+ # - MIN_POSITION <= gap[:end] <= MAX_POSITION
+ #
+ # Supposing the current item is 13, and that we have a sequence of items:
+ #
+ # 9 . . . [13] 14 15 . . . . 20 . . . 24
+ # ^---------^
+ #
+ # Then we return: `{ start: 15, end: 20 }`
+ #
+ # Here start refers to the end of the gap closest to the current item.
def find_next_gap_after
items_with_next_pos = scoped_items
.select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos')
.where('relative_position >= ?', relative_position)
.order(:relative_position)
- find_next_gap(items_with_next_pos).tap do |gap|
- gap[:end] ||= MAX_POSITION
- end
+ find_next_gap(items_with_next_pos, MAX_POSITION)
end
- def find_next_gap(items_with_next_pos)
- gap = self.class.from(items_with_next_pos, :items_with_next_pos)
- .where('ABS(pos - next_pos) > 1 OR next_pos IS NULL')
- .limit(1)
- .pluck(:pos, :next_pos)
- .first
+ def find_next_gap(items_with_next_pos, end_is_nil)
+ gap = self.class
+ .from(items_with_next_pos, :items)
+ .where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP)
+ .limit(1)
+ .pluck(:pos, :next_pos)
+ .first
+
+ return if gap.nil? || gap.first == end_is_nil
- { start: gap[0], end: gap[1] }
+ { start: gap.first, end: gap.second || end_is_nil }
end
def optimum_delta_for_gap(gap)
@@ -217,9 +463,10 @@ module RelativePositioning
[delta, IDEAL_DISTANCE].min
end
- def move_sequence(start_pos, end_pos, delta)
- scoped_items
- .where.not(id: self.id)
+ def move_sequence(start_pos, end_pos, delta, include_self = false)
+ relation = include_self ? scoped_items : relative_siblings
+
+ relation
.where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
.update_all("relative_position = relative_position + #{delta}")
end
@@ -240,6 +487,10 @@ module RelativePositioning
.first&.last
end
+ def relative_siblings(relation = scoped_items)
+ relation.id_not_in(id)
+ end
+
def scoped_items
self.class.relative_positioning_query_base(self)
end
diff --git a/app/models/concerns/sha_attribute.rb b/app/models/concerns/sha_attribute.rb
index c807dcbf418..cbac6a210c7 100644
--- a/app/models/concerns/sha_attribute.rb
+++ b/app/models/concerns/sha_attribute.rb
@@ -7,7 +7,7 @@ module ShaAttribute
def sha_attribute(name)
return if ENV['STATIC_VERIFICATION']
- validate_binary_column_exists!(name) unless Rails.env.production?
+ validate_binary_column_exists!(name) if Rails.env.development?
attribute(name, Gitlab::Database::ShaAttribute.new)
end
@@ -17,18 +17,11 @@ module ShaAttribute
# See https://gitlab.com/gitlab-org/gitlab/merge_requests/5502 for more discussion
def validate_binary_column_exists!(name)
return unless database_exists?
-
- unless table_exists?
- warn "WARNING: sha_attribute #{name.inspect} is invalid since the table doesn't exist - you may need to run database migrations"
- return
- end
+ return unless table_exists?
column = columns.find { |c| c.name == name.to_s }
- unless column
- warn "WARNING: sha_attribute #{name.inspect} is invalid since the column doesn't exist - you may need to run database migrations"
- return
- end
+ return unless column
unless column.type == :binary
raise ArgumentError.new("sha_attribute #{name.inspect} is invalid since the column type is not :binary")
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index dddf96837b7..a1e7d06b1c1 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -26,6 +26,7 @@ module TimeTrackable
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def spend_time(options)
@time_spent = options[:duration]
+ @time_spent_note_id = options[:note_id]
@time_spent_user = User.find(options[:user_id])
@spent_at = options[:spent_at]
@original_total_time_spent = nil
@@ -67,6 +68,7 @@ module TimeTrackable
def add_or_subtract_spent_time
timelogs.new(
time_spent: time_spent,
+ note_id: @time_spent_note_id,
user: @time_spent_user,
spent_at: @spent_at
)
diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb
index c52baa0524c..b64a9e4f70b 100644
--- a/app/models/concerns/triggerable_hooks.rb
+++ b/app/models/concerns/triggerable_hooks.rb
@@ -12,7 +12,8 @@ module TriggerableHooks
merge_request_hooks: :merge_requests_events,
job_hooks: :job_events,
pipeline_hooks: :pipeline_events,
- wiki_page_hooks: :wiki_page_events
+ wiki_page_hooks: :wiki_page_events,
+ deployment_hooks: :deployment_events
}.freeze
extend ActiveSupport::Concern
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
index c0fa14d3369..a7028e18451 100644
--- a/app/models/concerns/update_project_statistics.rb
+++ b/app/models/concerns/update_project_statistics.rb
@@ -75,7 +75,7 @@ module UpdateProjectStatistics
end
def schedule_update_project_statistic(delta)
- return if delta.zero?
+ return if delta == 0
return if project.nil?
run_after_commit do
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index aa3e3a8f66d..d6508ffceba 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -148,6 +148,7 @@ class Deployment < ApplicationRecord
def execute_hooks
deployment_data = Gitlab::DataBuilder::Deployment.build(self)
+ project.execute_hooks(deployment_data, :deployment_hooks) if Feature.enabled?(:deployment_webhooks, project, default_enabled: true)
project.execute_services(deployment_data, :deployment_hooks)
end
diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb
index 0dca6333fa1..deda814d689 100644
--- a/app/models/design_management/design.rb
+++ b/app/models/design_management/design.rb
@@ -9,6 +9,7 @@ module DesignManagement
include Referable
include Mentionable
include WhereComposite
+ include RelativePositioning
belongs_to :project, inverse_of: :designs
belongs_to :issue
@@ -75,9 +76,23 @@ module DesignManagement
join = designs.join(actions)
.on(actions[:design_id].eq(designs[:id]))
- joins(join.join_sources).where(actions[:event].not_eq(deletion)).order(:id)
+ joins(join.join_sources).where(actions[:event].not_eq(deletion))
end
+ scope :ordered, -> (project) do
+ # TODO: Always order by relative position after the feature flag is removed
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/34382
+ if Feature.enabled?(:reorder_designs, project, default_enabled: true)
+ # We need to additionally sort by `id` to support keyset pagination.
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17788/diffs#note_230875678
+ order(:relative_position, :id)
+ else
+ in_creation_order
+ end
+ end
+
+ scope :in_creation_order, -> { reorder(:id) }
+
scope :with_filename, -> (filenames) { where(filename: filenames) }
scope :on_issue, ->(issue) { where(issue_id: issue) }
@@ -87,6 +102,14 @@ module DesignManagement
# A design is current if the most recent event is not a deletion
scope :current, -> { visible_at_version(nil) }
+ def self.relative_positioning_query_base(design)
+ default_scoped.on_issue(design.issue_id)
+ end
+
+ def self.relative_positioning_parent_column
+ :issue_id
+ end
+
def status
if new_design?
:new
@@ -196,6 +219,17 @@ module DesignManagement
project
end
+ def immediately_before?(next_design)
+ return false if next_design.relative_position <= relative_position
+
+ interloper = self.class.on_issue(issue).where(
+ "relative_position <@ int4range(?, ?, '()')",
+ *[self, next_design].map(&:relative_position)
+ )
+
+ !interloper.exists?
+ end
+
private
def head_version
diff --git a/app/models/design_management/design_collection.rb b/app/models/design_management/design_collection.rb
index 18d1541e9c7..96d5f4c2419 100644
--- a/app/models/design_management/design_collection.rb
+++ b/app/models/design_management/design_collection.rb
@@ -10,6 +10,10 @@ module DesignManagement
@issue = issue
end
+ def ==(other)
+ other.is_a?(self.class) && issue == other.issue
+ end
+
def find_or_create_design!(filename:)
designs.find { |design| design.filename == filename } ||
designs.safe_find_or_create_by!(project: project, filename: filename)
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index e928bb0959a..adcb2217d85 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -23,6 +23,7 @@ class Discussion
:resolved_by_id,
:system_note_with_references_visible_for?,
:resource_parent,
+ :save,
to: :first_note
diff --git a/app/models/environment.rb b/app/models/environment.rb
index bddc84f10b5..c6a08c996da 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -29,6 +29,7 @@ class Environment < ApplicationRecord
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
+ has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
@@ -291,6 +292,10 @@ class Environment < ApplicationRecord
!!ENV['USE_SAMPLE_METRICS']
end
+ def has_opened_alert?
+ latest_opened_most_severe_alert.present?
+ end
+
def metrics
prometheus_adapter.query(:environment, self) if has_metrics_and_can_query?
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 56d7742c51a..92609144576 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -8,6 +8,7 @@ class Event < ApplicationRecord
include CreatedAtFilterable
include Gitlab::Utils::StrongMemoize
include UsageStatistics
+ include ShaAttribute
default_scope { reorder(nil) } # rubocop:disable Cop/DefaultScope
@@ -48,6 +49,8 @@ class Event < ApplicationRecord
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
REPOSITORY_UPDATED_AT_INTERVAL = 5.minutes
+ sha_attribute :fingerprint
+
enum action: ACTIONS, _suffix: true
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
@@ -82,6 +85,10 @@ class Event < ApplicationRecord
scope :recent, -> { reorder(id: :desc) }
scope :for_wiki_page, -> { where(target_type: 'WikiPage::Meta') }
scope :for_design, -> { where(target_type: 'DesignManagement::Design') }
+ scope :for_fingerprint, ->(fingerprint) do
+ fingerprint.present? ? where(fingerprint: fingerprint) : none
+ end
+ scope :for_action, ->(action) { where(action: action) }
scope :with_associations, -> do
# We're using preload for "push_event_payload" as otherwise the association
diff --git a/app/models/experiment.rb b/app/models/experiment.rb
new file mode 100644
index 00000000000..25640385536
--- /dev/null
+++ b/app/models/experiment.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class Experiment < ApplicationRecord
+ has_many :experiment_users
+ has_many :users, through: :experiment_users
+ has_many :control_group_users, -> { merge(ExperimentUser.control) }, through: :experiment_users, source: :user
+ has_many :experimental_group_users, -> { merge(ExperimentUser.experimental) }, through: :experiment_users, source: :user
+
+ validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
+
+ def self.add_user(name, group_type, user)
+ experiment = find_or_create_by(name: name)
+
+ return unless experiment
+ return if experiment.experiment_users.where(user: user).exists?
+
+ group_type == ::Gitlab::Experimentation::GROUP_CONTROL ? experiment.add_control_user(user) : experiment.add_experimental_user(user)
+ end
+
+ def add_control_user(user)
+ control_group_users << user
+ end
+
+ def add_experimental_user(user)
+ experimental_group_users << user
+ end
+end
diff --git a/app/models/experiment_user.rb b/app/models/experiment_user.rb
new file mode 100644
index 00000000000..1571b0c3439
--- /dev/null
+++ b/app/models/experiment_user.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class ExperimentUser < ApplicationRecord
+ belongs_to :experiment
+ belongs_to :user
+
+ enum group_type: { control: 0, experimental: 1 }
+
+ validates :group_type, presence: true
+end
diff --git a/app/models/external_pull_request.rb b/app/models/external_pull_request.rb
index 9c6d05f773a..1487a6387f0 100644
--- a/app/models/external_pull_request.rb
+++ b/app/models/external_pull_request.rb
@@ -63,6 +63,8 @@ class ExternalPullRequest < ApplicationRecord
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_IID', value: pull_request_iid.to_s)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY', value: source_repository)
+ variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY', value: target_repository)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA', value: source_sha)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA', value: target_sha)
variables.append(key: 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME', value: source_branch)
diff --git a/app/models/group.rb b/app/models/group.rb
index c38ddbdf6fb..f8cbaa2495c 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -64,6 +64,8 @@ class Group < Namespace
has_one :import_state, class_name: 'GroupImportState', inverse_of: :group
+ has_many :group_deploy_keys_groups, inverse_of: :group
+ has_many :group_deploy_keys, through: :group_deploy_keys_groups
has_many :group_deploy_tokens
has_many :deploy_tokens, through: :group_deploy_tokens
@@ -172,6 +174,10 @@ class Group < Namespace
notification_settings(hierarchy_order: hierarchy_order).where(user: user)
end
+ def packages_feature_enabled?
+ ::Gitlab.config.packages.enabled
+ end
+
def notification_email_for(user)
# Finds the closest notification_setting with a `notification_email`
notification_settings = notification_settings_for(user, hierarchy_order: :asc)
@@ -557,6 +563,10 @@ class Group < Namespace
all_projects.update_all(shared_runners_enabled: false)
end
+ def default_owner
+ owners.first || parent&.default_owner || owner
+ end
+
private
def update_two_factor_requirement
diff --git a/app/models/group_deploy_key.rb b/app/models/group_deploy_key.rb
index d1f1aa544cd..160ac28b33b 100644
--- a/app/models/group_deploy_key.rb
+++ b/app/models/group_deploy_key.rb
@@ -3,9 +3,31 @@
class GroupDeployKey < Key
self.table_name = 'group_deploy_keys'
+ has_many :group_deploy_keys_groups, inverse_of: :group_deploy_key, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :groups, through: :group_deploy_keys_groups
+
validates :user, presence: true
def type
'DeployKey'
end
+
+ def group_deploy_keys_group_for(group)
+ group_deploy_keys_groups.find_by(group: group)
+ end
+
+ def can_be_edited_for?(user, group)
+ Ability.allowed?(user, :update_group_deploy_key, self) ||
+ Ability.allowed?(
+ user,
+ :update_group_deploy_key_for_group,
+ group_deploy_keys_group_for(group)
+ )
+ end
+
+ def group_deploy_keys_groups_for_user(user)
+ group_deploy_keys_groups.select do |group_deploy_keys_group|
+ Ability.allowed?(user, :read_group, group_deploy_keys_group.group)
+ end
+ end
end
diff --git a/app/models/group_deploy_keys_group.rb b/app/models/group_deploy_keys_group.rb
new file mode 100644
index 00000000000..2fbfd2983b4
--- /dev/null
+++ b/app/models/group_deploy_keys_group.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class GroupDeployKeysGroup < ApplicationRecord
+ belongs_to :group, inverse_of: :group_deploy_keys_groups
+ belongs_to :group_deploy_key, inverse_of: :group_deploy_keys_groups
+
+ validates :group_deploy_key, presence: true
+ validates :group_deploy_key_id, uniqueness: { scope: [:group_id], message: "already exists in group" }
+ validates :group_id, presence: true
+end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 71494b6de4d..2d1bdecc770 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -17,7 +17,8 @@ class ProjectHook < WebHook
:merge_request_hooks,
:job_hooks,
:pipeline_hooks,
- :wiki_page_hooks
+ :wiki_page_hooks,
+ :deployment_hooks
]
belongs_to :project
diff --git a/app/models/individual_note_discussion.rb b/app/models/individual_note_discussion.rb
index bdfa6dcc6bd..c3ccf44d27e 100644
--- a/app/models/individual_note_discussion.rb
+++ b/app/models/individual_note_discussion.rb
@@ -17,12 +17,8 @@ class IndividualNoteDiscussion < Discussion
noteable.supports_replying_to_individual_notes?
end
- def convert_to_discussion!(save: false)
- first_note.becomes!(Discussion.note_class).to_discussion.tap do
- # Save needs to be called on first_note instead of the transformed note
- # because of https://gitlab.com/gitlab-org/gitlab-foss/issues/57324
- first_note.save if save
- end
+ def convert_to_discussion!
+ first_note.becomes!(Discussion.note_class).to_discussion
end
def reply_attributes
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 619555f369d..a0003df87e1 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -30,6 +30,8 @@ class Issue < ApplicationRecord
SORTING_PREFERENCE_FIELD = :issues_sort
belongs_to :project
+ has_one :namespace, through: :project
+
belongs_to :duplicated_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
belongs_to :iteration, foreign_key: 'sprint_id'
@@ -66,6 +68,12 @@ class Issue < ApplicationRecord
accepts_nested_attributes_for :sentry_issue
validates :project, presence: true
+ validates :issue_type, presence: true
+
+ enum issue_type: {
+ issue: 0,
+ incident: 1
+ }
alias_attribute :parent_ids, :project_id
alias_method :issuing_parent, :project
@@ -87,11 +95,18 @@ class Issue < ApplicationRecord
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
+ scope :with_web_entity_associations, -> { preload(:author, :project) }
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
scope :with_label_attributes, ->(label_attributes) { joins(:labels).where(labels: label_attributes) }
scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
scope :with_self_managed_prometheus_alert_events, -> { joins(:issues_self_managed_prometheus_alert_events) }
+ scope :with_api_entity_associations, -> {
+ preload(:timelogs, :closed_by, :assignees, :author, :notes, :labels,
+ milestone: { project: [:route, { namespace: :route }] },
+ project: [:route, { namespace: :route }])
+ }
+ scope :with_issue_type, ->(types) { where(issue_type: types) }
scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) }
@@ -146,10 +161,6 @@ class Issue < ApplicationRecord
issue.closed_at = nil
issue.closed_by = nil
end
-
- after_transition any => :closed do |issue|
- issue.resolve_associated_alert_management_alert
- end
end
# Alias to state machine .with_state_id method
@@ -363,18 +374,6 @@ class Issue < ApplicationRecord
@design_collection ||= ::DesignManagement::DesignCollection.new(self)
end
- def resolve_associated_alert_management_alert
- return unless alert_management_alert
- return if alert_management_alert.resolve
-
- Gitlab::AppLogger.warn(
- message: 'Cannot resolve an associated Alert Management alert',
- issue_id: id,
- alert_id: alert_management_alert.id,
- alert_errors: alert_management_alert.errors.messages
- )
- end
-
def from_service_desk?
author.id == User.support_bot.id
end
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index 0b59cf047f7..3495f099064 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -4,6 +4,7 @@ class Iteration < ApplicationRecord
self.table_name = 'sprints'
attr_accessor :skip_future_date_validation
+ attr_accessor :skip_project_validation
STATE_ENUM_MAP = {
upcoming: 1,
@@ -24,6 +25,7 @@ class Iteration < ApplicationRecord
validate :dates_do_not_overlap, if: :start_or_due_dates_changed?
validate :future_date, if: :start_or_due_dates_changed?, unless: :skip_future_date_validation
+ validate :no_project, unless: :skip_project_validation
scope :upcoming, -> { with_state(:upcoming) }
scope :started, -> { with_state(:started) }
@@ -113,6 +115,12 @@ class Iteration < ApplicationRecord
errors.add(:due_date, s_("Iteration|cannot be more than 500 years in the future")) if due_date > 500.years.from_now
end
end
+
+ def no_project
+ return unless project_id.present?
+
+ errors.add(:project_id, s_("is not allowed. We do not currently support project-level iterations"))
+ end
end
Iteration.prepend_if_ee('EE::Iteration')
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 3761484b15d..d60baa299cb 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -5,6 +5,7 @@ class LfsObject < ApplicationRecord
include Checksummable
include EachBatch
include ObjectStorage::BackgroundMove
+ include FileStoreMounter
has_many :lfs_objects_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, -> { distinct }, through: :lfs_objects_projects
@@ -15,21 +16,13 @@ class LfsObject < ApplicationRecord
validates :oid, presence: true, uniqueness: true
- mount_uploader :file, LfsObjectUploader
-
- after_save :update_file_store, if: :saved_change_to_file?
+ mount_file_store_uploader LfsObjectUploader
def self.not_linked_to_project(project)
where('NOT EXISTS (?)',
project.lfs_objects_projects.select(1).where('lfs_objects_projects.lfs_object_id = lfs_objects.id'))
end
- def update_file_store
- # The file.object_store is set during `uploader.store!`
- # which happens after object is inserted/updated
- self.update_column(:file_store, file.object_store)
- end
-
def project_allowed_access?(project)
if project.fork_network_member
lfs_objects_projects
diff --git a/app/models/member.rb b/app/models/member.rb
index 36f9741ce01..2c62ea55785 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -86,6 +86,7 @@ class Member < ApplicationRecord
scope :owners, -> { active.where(access_level: OWNER) }
scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) }
scope :with_user, -> (user) { where(user: user) }
+ scope :preload_user_and_notification_settings, -> { preload(user: :notification_settings) }
scope :with_source_id, ->(source_id) { where(source_id: source_id) }
scope :including_source, -> { includes(:source) }
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index b7885771781..f4c2d568b4d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -40,7 +40,7 @@ class MergeRequest < ApplicationRecord
has_internal_id :iid, scope: :target_project, track_if: -> { !importing? }, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
has_many :merge_request_diffs
- has_many :merge_request_context_commits
+ has_many :merge_request_context_commits, inverse_of: :merge_request
has_many :merge_request_context_commit_diff_files, through: :merge_request_context_commits, source: :diff_files
has_one :merge_request_diff,
@@ -251,17 +251,12 @@ class MergeRequest < ApplicationRecord
end
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
-
- PROJECT_ROUTE_AND_NAMESPACE_ROUTE = [
- target_project: [:route, { namespace: :route }],
- source_project: [:route, { namespace: :route }]
- ].freeze
-
scope :with_api_entity_associations, -> {
- preload(:assignees, :author, :unresolved_notes, :labels, :milestone,
- :timelogs, :latest_merge_request_diff,
- *PROJECT_ROUTE_AND_NAMESPACE_ROUTE,
- metrics: [:latest_closed_by, :merged_by])
+ preload_routables
+ .preload(:assignees, :author, :unresolved_notes, :labels, :milestone,
+ :timelogs, :latest_merge_request_diff,
+ target_project: :project_feature,
+ metrics: [:latest_closed_by, :merged_by])
}
scope :by_target_branch_wildcard, ->(wildcard_branch_name) do
@@ -269,6 +264,14 @@ class MergeRequest < ApplicationRecord
end
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
scope :preload_source_project, -> { preload(:source_project) }
+ scope :preload_target_project, -> { preload(:target_project) }
+ scope :preload_routables, -> do
+ preload(target_project: [:route, { namespace: :route }],
+ source_project: [:route, { namespace: :route }])
+ end
+ scope :preload_author, -> { preload(:author) }
+ scope :preload_approved_by_users, -> { preload(:approved_by_users) }
+ scope :preload_metrics, -> (relation) { preload(metrics: relation) }
scope :with_auto_merge_enabled, -> do
with_state(:opened).where(auto_merge_enabled: true)
@@ -428,7 +431,7 @@ class MergeRequest < ApplicationRecord
end
def context_commits(limit: nil)
- @context_commits ||= merge_request_context_commits.limit(limit).map(&:to_commit)
+ @context_commits ||= merge_request_context_commits.order_by_committed_date_desc.limit(limit).map(&:to_commit)
end
def recent_context_commits
@@ -1181,12 +1184,12 @@ class MergeRequest < ApplicationRecord
end
def can_be_merged_by?(user)
- access = ::Gitlab::UserAccess.new(user, project: project)
+ access = ::Gitlab::UserAccess.new(user, container: project)
access.can_update_branch?(target_branch)
end
def can_be_merged_via_command_line_by?(user)
- access = ::Gitlab::UserAccess.new(user, project: project)
+ access = ::Gitlab::UserAccess.new(user, container: project)
access.can_push_to_branch?(target_branch)
end
@@ -1608,7 +1611,12 @@ class MergeRequest < ApplicationRecord
override :ensure_metrics
def ensure_metrics
- MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id).tap do |metrics_record|
+ # Backward compatibility: some merge request metrics records will not have target_project_id filled in.
+ # In that case the first `safe_find_or_create_by` will return false.
+ # The second finder call will be eliminated in https://gitlab.com/gitlab-org/gitlab/-/issues/233507
+ metrics_record = MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id, target_project_id: target_project_id) || MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id)
+
+ metrics_record.tap do |metrics_record|
# Make sure we refresh the loaded association object with the newly created/loaded item.
# This is needed in order to have the exact functionality than before.
#
@@ -1618,6 +1626,8 @@ class MergeRequest < ApplicationRecord
# merge_request.ensure_metrics
# merge_request.metrics # should return the metrics record and not nil
# merge_request.metrics.merge_request # should return the same MR record
+
+ metrics_record.target_project_id = target_project_id
metrics_record.association(:merge_request).target = self
association(:metrics).target = metrics_record
end
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index ba363019c72..66bff3f5982 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -1,10 +1,21 @@
# frozen_string_literal: true
class MergeRequest::Metrics < ApplicationRecord
- belongs_to :merge_request
+ belongs_to :merge_request, inverse_of: :metrics
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
belongs_to :latest_closed_by, class_name: 'User'
belongs_to :merged_by, class_name: 'User'
+
+ before_save :ensure_target_project_id
+
+ scope :merged_after, ->(date) { where(arel_table[:merged_at].gteq(date)) }
+ scope :merged_before, ->(date) { where(arel_table[:merged_at].lteq(date)) }
+
+ private
+
+ def ensure_target_project_id
+ self.target_project_id ||= merge_request.target_project_id
+ end
end
MergeRequest::Metrics.prepend_if_ee('EE::MergeRequest::Metrics')
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index de97fc33f8d..a2982a5dd73 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -12,6 +12,9 @@ class MergeRequestContextCommit < ApplicationRecord
validates :sha, presence: true
validates :sha, uniqueness: { message: 'has already been added' }
+ # Sort by committed date in descending order to ensure latest commits comes on the top
+ scope :order_by_committed_date_desc, -> { order('committed_date DESC') }
+
# delete all MergeRequestContextCommit & MergeRequestContextCommitDiffFile for given merge_request & commit SHAs
def self.delete_bulk(merge_request, commits)
commit_ids = commits.map(&:sha)
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index eb5250d5cf6..b70340a98cd 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -51,14 +51,16 @@ class MergeRequestDiff < ApplicationRecord
scope :by_commit_sha, ->(sha) do
joins(:merge_request_diff_commits).where(merge_request_diff_commits: { sha: sha }).reorder(nil)
end
- scope :has_diff_files, -> { where(id: MergeRequestDiffFile.select(:merge_request_diff_id)) }
scope :by_project_id, -> (project_id) do
joins(:merge_request).where(merge_requests: { target_project_id: project_id })
end
scope :recent, -> { order(id: :desc).limit(100) }
- scope :files_in_database, -> { has_diff_files.where(stored_externally: [false, nil]) }
+
+ scope :files_in_database, -> do
+ where(stored_externally: [false, nil]).where(arel_table[:files_count].gt(0))
+ end
scope :not_latest_diffs, -> do
merge_requests = MergeRequest.arel_table
@@ -100,14 +102,25 @@ class MergeRequestDiff < ApplicationRecord
joins(merge_request: :metrics).where(condition)
end
- def self.ids_for_external_storage_migration(limit:)
- # No point doing any work unless the feature is enabled
- return [] unless Gitlab.config.external_diffs.enabled
+ class << self
+ def ids_for_external_storage_migration(limit:)
+ return [] unless Gitlab.config.external_diffs.enabled
- case Gitlab.config.external_diffs.when
- when 'always'
+ case Gitlab.config.external_diffs.when
+ when 'always'
+ ids_for_external_storage_migration_strategy_always(limit: limit)
+ when 'outdated'
+ ids_for_external_storage_migration_strategy_outdated(limit: limit)
+ else
+ []
+ end
+ end
+
+ def ids_for_external_storage_migration_strategy_always(limit:)
files_in_database.limit(limit).pluck(:id)
- when 'outdated'
+ end
+
+ def ids_for_external_storage_migration_strategy_outdated(limit:)
# Outdated is too complex to be a single SQL query, so split into three
before = EXTERNAL_DIFF_CUTOFF.ago
@@ -129,8 +142,6 @@ class MergeRequestDiff < ApplicationRecord
.not_latest_diffs
.limit(limit - ids.size)
.pluck(:id)
- else
- []
end
end
@@ -139,6 +150,7 @@ class MergeRequestDiff < ApplicationRecord
# All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff.
after_create :save_git_content, unless: :importing?
+ after_create :set_count_columns
after_create_commit :set_as_latest_diff, unless: :importing?
after_save :update_external_diff_store
@@ -621,7 +633,7 @@ class MergeRequestDiff < ApplicationRecord
def save_diffs
new_attributes = {}
- if compare.commits.size.zero?
+ if compare.commits.empty?
new_attributes[:state] = :empty
else
diff_collection = compare.diffs(Commit.max_diff_options)
@@ -632,6 +644,7 @@ class MergeRequestDiff < ApplicationRecord
rows = build_merge_request_diff_files(diff_collection)
create_merge_request_diff_files(rows)
+ self.class.uncached { merge_request_diff_files.reset }
end
# Set our state to 'overflow' to make the #empty? and #collected?
@@ -647,12 +660,14 @@ class MergeRequestDiff < ApplicationRecord
def save_commits
MergeRequestDiffCommit.create_bulk(self.id, compare.commits.reverse)
+ self.class.uncached { merge_request_diff_commits.reset }
+ end
- # merge_request_diff_commits.reset is preferred way to reload associated
- # objects but it returns cached result for some reason in this case
- # we can circumvent that by specifying that we need an uncached reload
- commits = self.class.uncached { merge_request_diff_commits.reset }
- self.commits_count = commits.size
+ def set_count_columns
+ update_columns(
+ commits_count: merge_request_diff_commits.size,
+ files_count: merge_request_diff_files.size
+ )
end
def repository
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 58adfd5f70b..55326b9a282 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -127,7 +127,7 @@ class Milestone < ApplicationRecord
end
def can_be_closed?
- active? && issues.opened.count.zero?
+ active? && issues.opened.count == 0
end
def author_id
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 6b5ea0fc3fc..9da454125eb 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -211,7 +211,7 @@ module Network
# Visit branching chains
leaves.each do |l|
- parents = l.parents(@map).select {|p| p.space.zero?}
+ parents = l.parents(@map).select {|p| p.space == 0}
parents.each do |p|
place_chain(p, l.time)
end
@@ -266,14 +266,14 @@ module Network
def take_left_leaves(raw_commit)
commit = @map[raw_commit.id]
leaves = []
- leaves.push(commit) if commit.space.zero?
+ leaves.push(commit) if commit.space == 0
loop do
- return leaves if commit.parents(@map).count.zero?
+ return leaves if commit.parents(@map).count == 0
commit = commit.parents(@map).first
- return leaves unless commit.space.zero?
+ return leaves unless commit.space == 0
leaves.push(commit)
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 2db7e4e406d..e1fc16818b3 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -61,7 +61,7 @@ class Note < ApplicationRecord
attr_accessor :commands_changes
# A special role that may be displayed on issuable's discussions
- attr_accessor :special_role
+ attr_reader :special_role
default_value_for :system, false
@@ -417,7 +417,7 @@ class Note < ApplicationRecord
end
def can_create_todo?
- # Skip system notes, and notes on project snippet
+ # Skip system notes, and notes on snippets
!system? && !for_snippet?
end
@@ -559,6 +559,10 @@ class Note < ApplicationRecord
(!system_note_with_references? || all_referenced_mentionables_allowed?(user)) && system_note_viewable_by?(user)
end
+ def parent_user
+ noteable.author if for_personal_snippet?
+ end
+
private
# Using this method followed by a call to `save` may result in ActiveRecord::RecordNotUnique exception
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 36b7cd64c73..6a6b2bb1b58 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -52,10 +52,9 @@ class NotificationRecipient
when :mention
@type == :mention
when :participating
- %i[failed_pipeline fixed_pipeline].include?(@custom_action) ||
- %i[participating mention].include?(@type)
+ participating_custom_action? || participating_or_mention?
when :custom
- custom_enabled? || %i[participating mention].include?(@type)
+ custom_enabled? || participating_or_mention?
when :watch
!excluded_watcher_action?
else
@@ -175,4 +174,12 @@ class NotificationRecipient
.where.not(level: NotificationSetting.levels[:global])
.first
end
+
+ def participating_custom_action?
+ %i[failed_pipeline fixed_pipeline moved_project].include?(@custom_action)
+ end
+
+ def participating_or_mention?
+ %i[participating mention].include?(@type)
+ end
end
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index c8c1f47c182..c003a20f0fc 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -46,7 +46,8 @@ class NotificationSetting < ApplicationRecord
:merge_merge_request,
:failed_pipeline,
:fixed_pipeline,
- :success_pipeline
+ :success_pipeline,
+ :moved_project
].freeze
# Update unfound_translations.rb when events are changed
@@ -96,7 +97,11 @@ class NotificationSetting < ApplicationRecord
alias_method :fixed_pipeline?, :fixed_pipeline
def event_enabled?(event)
- respond_to?(event) && !!public_send(event) # rubocop:disable GitlabSecurity/PublicSend
+ # We override these two attributes, so we can't use read_attribute
+ return failed_pipeline if event.to_sym == :failed_pipeline
+ return fixed_pipeline if event.to_sym == :fixed_pipeline
+
+ has_attribute?(event) && !!read_attribute(event)
end
def owns_notification_email
diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb
index 567b5a14603..4ebd96797db 100644
--- a/app/models/packages/package_file.rb
+++ b/app/models/packages/package_file.rb
@@ -15,6 +15,8 @@ class Packages::PackageFile < ApplicationRecord
validates :file, presence: true
validates :file_name, presence: true
+ validates :file_name, uniqueness: { scope: :package }, if: -> { package&.pypi? }
+
scope :recent, -> { order(id: :desc) }
scope :with_file_name, ->(file_name) { where(file_name: file_name) }
scope :with_file_name_like, ->(file_name) { where(arel_table[:file_name].matches(file_name)) }
@@ -37,20 +39,27 @@ class Packages::PackageFile < ApplicationRecord
update_project_statistics project_statistics_name: :packages_size
+ before_save :update_size_from_file
+
def update_file_metadata
# The file.object_store is set during `uploader.store!`
# which happens after object is inserted/updated
self.update_column(:file_store, file.object_store)
- self.update_column(:size, file.size) unless file.size == self.size
end
def download_path
- Gitlab::Routing.url_helpers.download_project_package_file_path(project, self) if ::Gitlab.ee?
+ Gitlab::Routing.url_helpers.download_project_package_file_path(project, self)
end
def local?
file_store == ::Packages::PackageFileUploader::Store::LOCAL
end
+
+ private
+
+ def update_size_from_file
+ self.size ||= file.size
+ end
end
-Packages::PackageFile.prepend_if_ee('EE::Packages::PackageFileGeo')
+Packages::PackageFile.prepend_if_ee('EE::Packages::PackageFile')
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 856496f0941..d071d2d3c89 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -3,6 +3,7 @@
class PagesDomain < ApplicationRecord
include Presentable
include FromUnion
+ include AfterCommitQueue
VERIFICATION_KEY = 'gitlab-pages-verification-code'
VERIFICATION_THRESHOLD = 3.days.freeze
@@ -222,6 +223,8 @@ class PagesDomain < ApplicationRecord
private
def pages_deployed?
+ return false unless project
+
# TODO: remove once `pages_metadatum` is migrated
# https://gitlab.com/gitlab-org/gitlab/issues/33106
unless project.pages_metadatum
@@ -244,8 +247,13 @@ class PagesDomain < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def update_daemon
return if usage_serverless?
+ return unless pages_deployed?
- ::Projects::UpdatePagesConfigurationService.new(project).execute
+ if Feature.enabled?(:async_update_pages_config, project)
+ run_after_commit { PagesUpdateConfigurationWorker.perform_async(project_id) }
+ else
+ Projects::UpdatePagesConfigurationService.new(project).execute
+ end
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 488ebd531a8..e01cb0530a5 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -19,6 +19,7 @@ class PersonalAccessToken < ApplicationRecord
scope :active, -> { where("revoked = false AND (expires_at >= CURRENT_DATE OR expires_at IS NULL)") }
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 :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb
index 197795dccfe..0915278fb65 100644
--- a/app/models/personal_snippet.rb
+++ b/app/models/personal_snippet.rb
@@ -3,6 +3,10 @@
class PersonalSnippet < Snippet
include WithUploads
+ def parent_user
+ author
+ end
+
def skip_project_check?
true
end
diff --git a/app/models/postgresql/replication_slot.rb b/app/models/postgresql/replication_slot.rb
index 7a123deb719..a4370eda5ba 100644
--- a/app/models/postgresql/replication_slot.rb
+++ b/app/models/postgresql/replication_slot.rb
@@ -33,7 +33,7 @@ module Postgresql
# If too many replicas are falling behind too much, the availability of a
# GitLab instance might suffer. To prevent this from happening we require
# at least 1 replica to have data recent enough.
- if sizes.any? && too_great.positive?
+ if sizes.any? && too_great > 0
(sizes.length - too_great) <= 1
else
false
diff --git a/app/models/product_analytics_event.rb b/app/models/product_analytics_event.rb
index 95a2e7a26c4..579ea88c272 100644
--- a/app/models/product_analytics_event.rb
+++ b/app/models/product_analytics_event.rb
@@ -19,4 +19,12 @@ class ProductAnalyticsEvent < ApplicationRecord
scope :timerange, ->(duration, today = Time.zone.today) {
where('collector_tstamp BETWEEN ? AND ? ', today - duration + 1, today + 1)
}
+
+ def self.count_by_graph(graph, days)
+ group(graph).timerange(days).count
+ end
+
+ def as_json_wo_empty
+ as_json.compact
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 3aa0db56404..e1b6a9c41dd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -109,7 +109,6 @@ class Project < ApplicationRecord
after_update :update_forks_visibility_level
before_destroy :remove_private_deploy_keys
- before_destroy :cleanup_chat_names
use_fast_destroy :build_trace_chunks
@@ -168,7 +167,6 @@ class Project < ApplicationRecord
has_one :youtrack_service
has_one :custom_issue_tracker_service
has_one :bugzilla_service
- has_one :gitlab_issue_tracker_service, inverse_of: :project
has_one :confluence_service
has_one :external_wiki_service
has_one :prometheus_service, inverse_of: :project
@@ -261,6 +259,7 @@ class Project < ApplicationRecord
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :kubernetes_namespaces, class_name: 'Clusters::KubernetesNamespace'
has_many :management_clusters, class_name: 'Clusters::Cluster', foreign_key: :management_project_id, inverse_of: :management_project
+ has_many :cluster_agents, class_name: 'Clusters::Agent'
has_many :prometheus_metrics
has_many :prometheus_alerts, inverse_of: :project
@@ -300,6 +299,7 @@ class Project < ApplicationRecord
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :build_report_results, class_name: 'Ci::BuildReportResult', inverse_of: :project
has_many :job_artifacts, class_name: 'Ci::JobArtifact'
+ has_many :pipeline_artifacts, class_name: 'Ci::PipelineArtifact', inverse_of: :project
has_many :runner_projects, class_name: 'Ci::RunnerProject', inverse_of: :project
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, class_name: 'Ci::Variable'
@@ -339,6 +339,10 @@ class Project < ApplicationRecord
has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project
has_many :reviews, inverse_of: :project
+ # Can be too many records. We need to implement delete_all in batches.
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/228637
+ has_many :product_analytics_events, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :project_setting, update_only: true
@@ -450,6 +454,16 @@ class Project < ApplicationRecord
# Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name
scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) }
+ scope :sorted_by_similarity_desc, -> (search) do
+ order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
+ { column: arel_table["path"], multiplier: 1 },
+ { column: arel_table["name"], multiplier: 0.7 },
+ { column: arel_table["description"], multiplier: 0.2 }
+ ])
+
+ reorder(order_expression.desc, arel_table['id'].desc)
+ end
+
scope :with_packages, -> { joins(:packages) }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
@@ -637,6 +651,8 @@ class Project < ApplicationRecord
scope :joins_import_state, -> { joins("INNER JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
scope :for_group, -> (group) { where(group: group) }
scope :for_group_and_its_subgroups, ->(group) { where(namespace_id: group.self_and_descendants.select(:id)) }
+ scope :for_repository_storage, -> (repository_storage) { where(repository_storage: repository_storage) }
+ scope :excluding_repository_storage, -> (repository_storage) { where.not(repository_storage: repository_storage) }
class << self
# Searches for a list of projects based on the query given in `query`.
@@ -838,6 +854,10 @@ class Project < ApplicationRecord
auto_devops_config[:scope] != :project && !auto_devops_config[:status]
end
+ def has_packages?(package_type)
+ packages.where(package_type: package_type).exists?
+ end
+
def first_auto_devops_config
return namespace.first_auto_devops_config if auto_devops&.enabled.nil?
@@ -1103,7 +1123,7 @@ class Project < ApplicationRecord
limit = creator.projects_limit
error =
- if limit.zero?
+ if limit == 0
_('Personal project creation is not allowed. Please contact your administrator with questions')
else
_('Your project limit is %{limit} projects! Please contact your administrator to increase it')
@@ -1375,6 +1395,16 @@ class Project < ApplicationRecord
group || namespace.try(:owner)
end
+ def default_owner
+ obj = owner
+
+ if obj.respond_to?(:default_owner)
+ obj.default_owner
+ else
+ obj
+ end
+ end
+
def to_ability_name
model_name.singular
end
@@ -1725,7 +1755,7 @@ class Project < ApplicationRecord
end
def pages_deployed?
- Dir.exist?(public_pages_path)
+ pages_metadatum&.deployed?
end
def pages_group_url
@@ -1758,10 +1788,6 @@ class Project < ApplicationRecord
File.join(Settings.pages.path, full_path)
end
- def public_pages_path
- File.join(pages_path, 'public')
- end
-
def pages_available?
Gitlab.config.pages.enabled
end
@@ -1788,7 +1814,6 @@ class Project < ApplicationRecord
return unless namespace
mark_pages_as_not_deployed unless destroyed?
- ::Projects::UpdatePagesConfigurationService.new(self).execute
# 1. We rename pages to temporary directory
# 2. We wait 5 minutes, due to NFS caching
@@ -1926,17 +1951,6 @@ class Project < ApplicationRecord
import_export_upload&.export_file
end
- # Before 12.9 we did not correctly clean up chat names and this causes issues.
- # In 12.9, we add a foreign key relationship, but this code is used ensure the chat names are cleaned up while a post
- # migration enables the foreign key relationship.
- #
- # This should be removed in 13.0.
- #
- # https://gitlab.com/gitlab-org/gitlab/issues/204787
- def cleanup_chat_names
- ChatName.where(service: services.select(:id)).delete_all
- end
-
def full_path_slug
Gitlab::Utils.slugify(full_path.to_s)
end
@@ -2466,6 +2480,10 @@ class Project < ApplicationRecord
alias_method :service_desk_enabled?, :service_desk_enabled
def service_desk_address
+ service_desk_custom_address || service_desk_incoming_address
+ end
+
+ def service_desk_incoming_address
return unless service_desk_enabled?
config = Gitlab.config.incoming_email
@@ -2474,6 +2492,16 @@ class Project < ApplicationRecord
config.address&.gsub(wildcard, "#{full_path_slug}-#{id}-issue-")
end
+ def service_desk_custom_address
+ return unless ::Gitlab::ServiceDeskEmail.enabled?
+ return unless ::Feature.enabled?(:service_desk_custom_address, self)
+
+ key = service_desk_setting&.project_key
+ return unless key.present?
+
+ ::Gitlab::ServiceDeskEmail.address_for_key("#{full_path_slug}-#{key}")
+ end
+
def root_namespace
if namespace.has_parent?
namespace.root_ancestor
@@ -2578,6 +2606,8 @@ class Project < ApplicationRecord
namespace != from.namespace
when Namespace
namespace != from
+ when User
+ true
end
end
diff --git a/app/models/project_repository_storage_move.rb b/app/models/project_repository_storage_move.rb
index b18d9765a57..2b74d9ccd88 100644
--- a/app/models/project_repository_storage_move.rb
+++ b/app/models/project_repository_storage_move.rb
@@ -29,12 +29,17 @@ class ProjectRepositoryStorageMove < ApplicationRecord
transition scheduled: :started
end
- event :finish do
- transition started: :finished
+ event :finish_replication do
+ transition started: :replicated
+ end
+
+ event :finish_cleanup do
+ transition replicated: :finished
end
event :do_fail do
transition [:initial, :scheduled, :started] => :failed
+ transition replicated: :cleanup_failed
end
after_transition initial: :scheduled do |storage_move|
@@ -49,7 +54,7 @@ class ProjectRepositoryStorageMove < ApplicationRecord
end
end
- after_transition started: :finished do |storage_move|
+ after_transition started: :replicated do |storage_move|
storage_move.project.update_columns(
repository_read_only: false,
repository_storage: storage_move.destination_storage_name
@@ -65,6 +70,8 @@ class ProjectRepositoryStorageMove < ApplicationRecord
state :started, value: 3
state :finished, value: 4
state :failed, value: 5
+ state :replicated, value: 6
+ state :cleanup_failed, value: 7
end
scope :order_created_at_desc, -> { order(created_at: :desc) }
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index fc7a0180786..53bb7b47b41 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -8,13 +8,32 @@ class BuildkiteService < CiService
ENDPOINT = "https://buildkite.com"
prop_accessor :project_url, :token
- boolean_accessor :enable_ssl_verification
validates :project_url, presence: true, public_url: true, if: :activated?
validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
+ def self.supported_events
+ %w(push merge_request tag_push)
+ end
+
+ # This is a stub method to work with deprecated API response
+ # TODO: remove enable_ssl_verification after 14.0
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/222808
+ def enable_ssl_verification
+ true
+ end
+
+ # Since SSL verification will always be enabled for Buildkite,
+ # we no longer needs to store the boolean.
+ # This is a stub method to work with deprecated API param.
+ # TODO: remove enable_ssl_verification after 14.0
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/222808
+ def enable_ssl_verification=(_value)
+ self.properties.delete('enable_ssl_verification') # Remove unused key
+ end
+
def webhook_url
"#{buildkite_endpoint('webhook')}/deliver/#{webhook_token}"
end
@@ -22,7 +41,7 @@ class BuildkiteService < CiService
def compose_service_hook
hook = service_hook || build_service_hook
hook.url = webhook_url
- hook.enable_ssl_verification = !!enable_ssl_verification
+ hook.enable_ssl_verification = true
hook.save
end
@@ -49,7 +68,7 @@ class BuildkiteService < CiService
end
def description
- 'Continuous integration and deployments'
+ 'Buildkite is a platform for running fast, secure, and scalable continuous integration pipelines on your own infrastructure'
end
def self.to_param
@@ -60,15 +79,15 @@ class BuildkiteService < CiService
[
{ type: 'text',
name: 'token',
- placeholder: 'Buildkite project GitLab token', required: true },
+ title: 'Integration Token',
+ help: 'This token will be provided when you create a Buildkite pipeline with a GitLab repository',
+ required: true },
{ type: 'text',
name: 'project_url',
- placeholder: "#{ENDPOINT}/example/project", required: true },
-
- { type: 'checkbox',
- name: 'enable_ssl_verification',
- title: "Enable SSL verification" }
+ title: 'Pipeline URL',
+ placeholder: "#{ENDPOINT}/acme-inc/test-pipeline",
+ required: true }
]
end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
deleted file mode 100644
index b3f44e040bc..00000000000
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-class GitlabIssueTrackerService < IssueTrackerService
- include Gitlab::Routing
-
- validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
-
- default_value_for :default, true
-
- def title
- 'GitLab'
- end
-
- def description
- s_('IssueTracker|GitLab issue tracker')
- end
-
- def self.to_param
- 'gitlab'
- end
-
- def project_url
- project_issues_url(project)
- end
-
- def new_issue_url
- new_project_issue_url(project)
- end
-
- def issue_url(iid)
- project_issue_url(project, id: iid)
- end
-
- def issue_tracker_path
- project_issues_path(project)
- end
-
- def new_issue_path
- new_project_issue_path(project)
- end
-
- def issue_path(iid)
- project_issue_path(project, id: iid)
- end
-end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 4ea2ec10f11..36d7026de30 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -8,6 +8,12 @@ class JiraService < IssueTrackerService
PROJECTS_PER_PAGE = 50
+ # TODO: use jira_service.deployment_type enum when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
+ DEPLOYMENT_TYPES = {
+ server: 'SERVER',
+ cloud: 'CLOUD'
+ }.freeze
+
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
validates :username, presence: true, if: :activated?
@@ -375,7 +381,6 @@ class JiraService < IssueTrackerService
def build_entity_url(noteable_type, entity_id)
polymorphic_url(
[
- self.project.namespace.becomes(Namespace),
self.project,
noteable_type.to_sym
],
diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb
index f24ba8877d2..00b6ab6a70f 100644
--- a/app/models/project_services/jira_tracker_data.rb
+++ b/app/models/project_services/jira_tracker_data.rb
@@ -7,4 +7,6 @@ class JiraTrackerData < ApplicationRecord
attr_encrypted :api_url, encryption_options
attr_encrypted :username, encryption_options
attr_encrypted :password, encryption_options
+
+ enum deployment_type: { unknown: 0, server: 1, cloud: 2 }, _prefix: :deployment
end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 997c6eba91a..950cd4f6859 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -97,7 +97,13 @@ class PrometheusService < MonitoringService
def prometheus_client
return unless should_return_client?
- options = { allow_local_requests: allow_local_api_url? }
+ options = {
+ allow_local_requests: allow_local_api_url?,
+ # We should choose more conservative timeouts, but some queries we run are now busting our
+ # default timeouts, which are stricter. We should make those queries faster instead.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/233109
+ timeout: 60
+ }
if behind_iap?
# Adds the Authorization header
diff --git a/app/models/prometheus_alert.rb b/app/models/prometheus_alert.rb
index 32f9809e538..f0441d4a3cb 100644
--- a/app/models/prometheus_alert.rb
+++ b/app/models/prometheus_alert.rb
@@ -3,6 +3,7 @@
class PrometheusAlert < ApplicationRecord
include Sortable
include UsageStatistics
+ include Presentable
OPERATORS_MAP = {
lt: "<",
@@ -21,7 +22,9 @@ class PrometheusAlert < ApplicationRecord
after_save :clear_prometheus_adapter_cache!
after_destroy :clear_prometheus_adapter_cache!
- validates :environment, :project, :prometheus_metric, presence: true
+ validates :environment, :project, :prometheus_metric, :threshold, :operator, presence: true
+ validates :runbook_url, length: { maximum: 255 }, allow_blank: true,
+ addressable_url: { enforce_sanitization: true, ascii_only: true }
validate :require_valid_environment_project!
validate :require_valid_metric_project!
@@ -59,6 +62,9 @@ class PrometheusAlert < ApplicationRecord
"gitlab" => "hook",
"gitlab_alert_id" => prometheus_metric_id,
"gitlab_prometheus_alert_id" => id
+ },
+ "annotations" => {
+ "runbook" => runbook_url
}
}
end
diff --git a/app/models/raw_usage_data.rb b/app/models/raw_usage_data.rb
new file mode 100644
index 00000000000..18cee55d06e
--- /dev/null
+++ b/app/models/raw_usage_data.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class RawUsageData < ApplicationRecord
+ validates :payload, presence: true
+ validates :recorded_at, presence: true, uniqueness: true
+
+ def update_sent_at!
+ self.update_column(:sent_at, Time.current) if Feature.enabled?(:save_raw_usage_data)
+ end
+end
diff --git a/app/models/release.rb b/app/models/release.rb
index a0245105cd9..4c9d89105d7 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -18,12 +18,10 @@ class Release < ApplicationRecord
has_many :milestones, through: :milestone_releases
has_many :evidences, inverse_of: :release, class_name: 'Releases::Evidence'
- default_value_for :released_at, allows_nil: false do
- Time.zone.now
- end
-
accepts_nested_attributes_for :links, allow_destroy: true
+ before_create :set_released_at
+
validates :project, :tag, presence: true
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
@@ -90,6 +88,10 @@ class Release < ApplicationRecord
repository.find_tag(tag)
end
end
+
+ def set_released_at
+ self.released_at ||= created_at
+ end
end
Release.prepend_if_ee('EE::Release')
diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb
index dc7e78a85a9..e1dc3b904b9 100644
--- a/app/models/releases/link.rb
+++ b/app/models/releases/link.rb
@@ -6,7 +6,7 @@ module Releases
belongs_to :release
- FILEPATH_REGEX = /\A\/([\-\.\w]+\/?)*[\da-zA-Z]+\z/.freeze
+ FILEPATH_REGEX = %r{\A/(?:[\-\.\w]+/?)*[\da-zA-Z]+\z}.freeze
validates :url, presence: true, addressable_url: { schemes: %w(http https ftp) }, uniqueness: { scope: :release }
validates :name, presence: true, uniqueness: { scope: :release }
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 48e96d4c193..07122db36b3 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -43,7 +43,7 @@ class Repository
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref merged_branch_names
has_visible_content? issue_template_names merge_request_template_names
- metrics_dashboard_paths xcode_project?).freeze
+ 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
@@ -61,7 +61,7 @@ class Repository
avatar: :avatar,
issue_template: :issue_template_names,
merge_request_template: :merge_request_template_names,
- metrics_dashboard: :metrics_dashboard_paths,
+ metrics_dashboard: :user_defined_metrics_dashboard_paths,
xcode_config: :xcode_project?
}.freeze
@@ -196,6 +196,32 @@ class Repository
tag_exists?(ref) && branch_exists?(ref)
end
+ # It's possible for a tag name to be a prefix (including slash) of a branch
+ # name, or vice versa. For instance, a tag named `foo` means we can't create a
+ # tag `foo/bar`, but we _can_ create a branch `foo/bar`.
+ #
+ # If we know a repository has no refs of this type (which is the common case)
+ # then separating refs from paths - as in ExtractsRef - can be faster.
+ #
+ # This method only checks one level deep, so only prefixes that contain no
+ # slashes are considered. If a repository has a tag `foo/bar` and a branch
+ # `foo/bar/baz`, it will return false.
+ def has_ambiguous_refs?
+ return false unless branch_names.present? && tag_names.present?
+
+ with_slash, no_slash = (branch_names + tag_names).partition { |ref| ref.include?('/') }
+
+ return false if with_slash.empty?
+
+ prefixes = no_slash.map { |ref| Regexp.escape(ref) }.join('|')
+ prefix_regex = %r{^#{prefixes}/}
+
+ with_slash.any? do |ref|
+ prefix_regex.match?(ref)
+ end
+ end
+ cache_method :has_ambiguous_refs?
+
def expand_ref(ref)
if tag_exists?(ref)
Gitlab::Git::TAG_REF_PREFIX + ref
@@ -286,14 +312,16 @@ class Repository
end
def expire_tags_cache
- expire_method_caches(%i(tag_names tag_count))
+ expire_method_caches(%i(tag_names tag_count has_ambiguous_refs?))
@tags = nil
+ @tag_names_include = nil
end
def expire_branches_cache
- expire_method_caches(%i(branch_names merged_branch_names branch_count has_visible_content?))
+ expire_method_caches(%i(branch_names merged_branch_names branch_count has_visible_content? has_ambiguous_refs?))
@local_branches = nil
@branch_exists_memo = nil
+ @branch_names_include = nil
end
def expire_statistics_caches
@@ -576,10 +604,10 @@ class Repository
end
cache_method :merge_request_template_names, fallback: []
- def metrics_dashboard_paths
- Gitlab::Metrics::Dashboard::Finder.find_all_paths_from_source(project)
+ def user_defined_metrics_dashboard_paths
+ Gitlab::Metrics::Dashboard::RepoDashboardFinder.list_dashboards(project)
end
- cache_method :metrics_dashboard_paths
+ cache_method :user_defined_metrics_dashboard_paths, fallback: []
def readme
head_tree&.readme
@@ -852,7 +880,7 @@ class Repository
def revert(
user, commit, branch_name, message,
- start_branch_name: nil, start_project: project)
+ start_branch_name: nil, start_project: project, dry_run: false)
with_cache_hooks do
raw_repository.revert(
@@ -861,14 +889,15 @@ class Repository
branch_name: branch_name,
message: message,
start_branch_name: start_branch_name,
- start_repository: start_project.repository.raw_repository
+ start_repository: start_project.repository.raw_repository,
+ dry_run: dry_run
)
end
end
def cherry_pick(
user, commit, branch_name, message,
- start_branch_name: nil, start_project: project)
+ start_branch_name: nil, start_project: project, dry_run: false)
with_cache_hooks do
raw_repository.cherry_pick(
@@ -877,7 +906,8 @@ class Repository
branch_name: branch_name,
message: message,
start_branch_name: start_branch_name,
- start_repository: start_project.repository.raw_repository
+ start_repository: start_project.repository.raw_repository,
+ dry_run: dry_run
)
end
end
diff --git a/app/models/resource_iteration_event.rb b/app/models/resource_iteration_event.rb
new file mode 100644
index 00000000000..78d85ea8b95
--- /dev/null
+++ b/app/models/resource_iteration_event.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ResourceIterationEvent < ResourceTimeboxEvent
+ belongs_to :iteration
+end
diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb
index 36068cf508b..5fd71612de0 100644
--- a/app/models/resource_milestone_event.rb
+++ b/app/models/resource_milestone_event.rb
@@ -1,30 +1,17 @@
# frozen_string_literal: true
-class ResourceMilestoneEvent < ResourceEvent
+class ResourceMilestoneEvent < ResourceTimeboxEvent
include IgnorableColumns
- include IssueResourceEvent
- include MergeRequestResourceEvent
belongs_to :milestone
- validate :exactly_one_issuable
-
scope :include_relations, -> { includes(:user, milestone: [:project, :group]) }
- enum action: {
- add: 1,
- remove: 2
- }
-
# state is used for issue and merge request states.
enum state: Issue.available_states.merge(MergeRequest.available_states)
ignore_columns %i[reference reference_html cached_markdown_version], remove_with: '13.1', remove_after: '2020-06-22'
- def self.issuable_attrs
- %i(issue merge_request).freeze
- end
-
def milestone_title
milestone&.title
end
@@ -32,8 +19,4 @@ class ResourceMilestoneEvent < ResourceEvent
def milestone_parent
milestone&.parent
end
-
- def issuable
- issue || merge_request
- end
end
diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb
new file mode 100644
index 00000000000..44f48915425
--- /dev/null
+++ b/app/models/resource_timebox_event.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class ResourceTimeboxEvent < ResourceEvent
+ self.abstract_class = true
+
+ include IssueResourceEvent
+ include MergeRequestResourceEvent
+
+ validate :exactly_one_issuable
+
+ enum action: {
+ add: 1,
+ remove: 2
+ }
+
+ def self.issuable_attrs
+ %i(issue merge_request).freeze
+ end
+
+ def issuable
+ issue || merge_request
+ end
+end
diff --git a/app/models/service.rb b/app/models/service.rb
index 89bde61bfe1..40e7e5552d1 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -10,6 +10,7 @@ class Service < ApplicationRecord
include IgnorableColumns
ignore_columns %i[title description], remove_with: '13.4', remove_after: '2020-09-22'
+ ignore_columns %i[default], remove_with: '13.5', remove_after: '2020-10-22'
SERVICE_NAMES = %w[
alerts asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker discord
@@ -47,19 +48,20 @@ class Service < ApplicationRecord
belongs_to :project, inverse_of: :services
has_one :service_hook
- validates :project_id, presence: true, unless: -> { template? || instance? }
- validates :project_id, absence: true, if: -> { template? || instance? }
- validates :type, uniqueness: { scope: :project_id }, unless: -> { template? || instance? }, on: :create
+ validates :project_id, presence: true, unless: -> { template? || instance? || group_id }
+ validates :group_id, presence: true, unless: -> { template? || instance? || project_id }
+ validates :project_id, :group_id, absence: true, if: -> { template? || instance? }
+ validates :type, uniqueness: { scope: :project_id }, unless: -> { template? || instance? || group_id }, on: :create
+ validates :type, uniqueness: { scope: :group_id }, unless: -> { template? || instance? || project_id }
validates :type, presence: true
validates :template, uniqueness: { scope: :type }, if: -> { template? }
validates :instance, uniqueness: { scope: :type }, if: -> { instance? }
validate :validate_is_instance_or_template
+ validate :validate_belongs_to_project_or_group
- scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
- scope :issue_trackers, -> { where(category: 'issue_tracker') }
+ scope :external_issue_trackers, -> { where(category: 'issue_tracker').active }
scope :external_wikis, -> { where(type: 'ExternalWikiService').active }
scope :active, -> { where(active: true) }
- scope :without_defaults, -> { where(default: false) }
scope :by_type, -> (type) { where(type: type) }
scope :by_active_flag, -> (flag) { where(active: flag) }
scope :templates, -> { where(template: true, type: available_services_types) }
@@ -77,7 +79,6 @@ class Service < ApplicationRecord
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
scope :alert_hooks, -> { where(alert_events: true, active: true) }
- scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') }
default_value_for :category, 'common'
@@ -379,6 +380,10 @@ class Service < ApplicationRecord
errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance?
end
+ def validate_belongs_to_project_or_group
+ errors.add(:project_id, 'The service cannot belong to both a project and a group') if project_id && group_id
+ end
+
def cache_project_has_external_issue_tracker
if project && !project.destroyed?
project.cache_has_external_issue_tracker
diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb
index 94f3a140098..8c72bd5ae7e 100644
--- a/app/models/suggestion.rb
+++ b/app/models/suggestion.rb
@@ -43,12 +43,12 @@ class Suggestion < ApplicationRecord
def inapplicable_reason(cached: true)
strong_memoize("inapplicable_reason_#{cached}") do
- next :applied if applied?
- next :merge_request_merged if noteable.merged?
- next :merge_request_closed if noteable.closed?
- next :source_branch_deleted unless noteable.source_branch_exists?
- next :outdated if outdated?(cached: cached) || !note.active?
- next :same_content unless different_content?
+ next _("Can't apply this suggestion.") if applied?
+ next _("This merge request was merged. To apply this suggestion, edit this file directly.") if noteable.merged?
+ next _("This merge request is closed. To apply this suggestion, edit this file directly.") if noteable.closed?
+ next _("Can't apply as the source branch was deleted.") unless noteable.source_branch_exists?
+ next outdated_reason if outdated?(cached: cached) || !note.active?
+ next _("This suggestion already matches its content.") unless different_content?
end
end
@@ -61,7 +61,7 @@ class Suggestion < ApplicationRecord
end
def single_line?
- lines_above.zero? && lines_below.zero?
+ lines_above == 0 && lines_below == 0
end
def target_line
@@ -73,4 +73,12 @@ class Suggestion < ApplicationRecord
def different_content?
from_content != to_content
end
+
+ def outdated_reason
+ if single_line?
+ _("Can't apply as this line was changed in a more recent version.")
+ else
+ _("Can't apply as these lines were changed in a more recent version.")
+ end
+ end
end
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index 6ed074b2190..c50b9da1310 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -3,6 +3,7 @@
module Terraform
class State < ApplicationRecord
include UsageStatistics
+ include FileStoreMounter
DEFAULT = '{"version":1}'.freeze
HEX_REGEXP = %r{\A\h+\z}.freeze
@@ -17,24 +18,22 @@ module Terraform
default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) }
- after_save :update_file_store, if: :saved_change_to_file?
-
- mount_uploader :file, StateUploader
+ mount_file_store_uploader StateUploader
default_value_for(:file) { CarrierWaveStringFile.new(DEFAULT) }
- def update_file_store
- # The file.object_store is set during `uploader.store!`
- # which happens after object is inserted/updated
- self.update_column(:file_store, file.object_store)
- end
-
def file_store
super || StateUploader.default_store
end
+ def local?
+ file_store == ObjectStorage::Store::LOCAL
+ end
+
def locked?
self.lock_xid.present?
end
end
end
+
+Terraform::State.prepend_if_ee('EE::Terraform::State')
diff --git a/app/models/user.rb b/app/models/user.rb
index 643b759e6f4..1a67116c1f2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -104,6 +104,7 @@ class User < ApplicationRecord
# Profile
has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
+ has_many :group_deploy_keys
has_many :gpg_keys
has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -350,11 +351,25 @@ class User < ApplicationRecord
.without_impersonation
.expiring_and_not_notified(at).select(1))
end
+ scope :with_personal_access_tokens_expired_today, -> do
+ where('EXISTS (?)',
+ ::PersonalAccessToken
+ .select(1)
+ .where('personal_access_tokens.user_id = users.id')
+ .without_impersonation
+ .expired_today_and_not_notified)
+ end
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
+ def preferred_language
+ read_attribute('preferred_language') ||
+ I18n.default_locale.to_s.presence_in(Gitlab::I18n::AVAILABLE_LANGUAGES.keys) ||
+ 'en'
+ end
+
def active_for_authentication?
super && can?(:log_in)
end
@@ -948,7 +963,7 @@ class User < ApplicationRecord
def require_ssh_key?
count = Users::KeysCountService.new(self).count
- count.zero? && Gitlab::ProtocolAccess.allowed?('ssh')
+ count == 0 && Gitlab::ProtocolAccess.allowed?('ssh')
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/models/user_callout_enums.rb b/app/models/user_callout_enums.rb
index 226c8cd9ab5..5b64befd284 100644
--- a/app/models/user_callout_enums.rb
+++ b/app/models/user_callout_enums.rb
@@ -18,7 +18,9 @@ module UserCalloutEnums
tabs_position_highlight: 10,
webhooks_moved: 13,
admin_integrations_moved: 15,
- personal_access_token_expiry: 21 # EE-only
+ personal_access_token_expiry: 21, # EE-only
+ suggest_pipeline: 22,
+ customize_homepage: 23
}
end
end
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 4c497cc304c..30273d646cf 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -35,6 +35,7 @@ class Wiki
def initialize(container, user = nil)
@container = container
@user = user
+ raise ArgumentError, "user must be a User, got #{user.class}" if user && !user.is_a?(User)
end
def path
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 3dc90edb331..faf3d19d936 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -65,6 +65,7 @@ class WikiPage
validates :title, presence: true
validates :content, presence: true
validate :validate_path_limits, if: :title_changed?
+ validate :validate_content_size_limit, if: :content_changed?
# The GitLab Wiki instance.
attr_reader :wiki
@@ -97,6 +98,7 @@ class WikiPage
def slug
attributes[:slug].presence || wiki.wiki.preview_slug(title, format)
end
+ alias_method :id, :slug # required to use build_stubbed
alias_method :to_param, :slug
@@ -264,8 +266,8 @@ class WikiPage
'../shared/wikis/wiki_page'
end
- def id
- page.version.to_s
+ def sha
+ page.version&.sha
end
def title_changed?
@@ -282,6 +284,17 @@ class WikiPage
end
end
+ def content_changed?
+ if persisted?
+ # gollum-lib always converts CRLFs to LFs in Gollum::Wiki#normalize,
+ # so we need to do the same here.
+ # Also see https://gitlab.com/gitlab-org/gitlab/-/issues/21431
+ raw_content.delete("\r") != page&.text_data
+ else
+ raw_content.present?
+ end
+ end
+
# Updates the current @attributes hash by merging a hash of params
def update_attributes(attrs)
attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
@@ -391,4 +404,15 @@ class WikiPage
})
end
end
+
+ def validate_content_size_limit
+ current_value = raw_content.to_s.bytesize
+ max_size = Gitlab::CurrentSettings.wiki_page_max_content_bytes
+ return if current_value <= max_size
+
+ errors.add(:content, _('is too long (%{current_value}). The maximum size is %{max_size}.') % {
+ current_value: ActiveSupport::NumberHelper.number_to_human_size(current_value),
+ max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size)
+ })
+ end
end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 0879a740f8a..cc66ad0577d 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -3,7 +3,7 @@
module Ci
class BuildPolicy < CommitStatusPolicy
condition(:protected_ref) do
- access = ::Gitlab::UserAccess.new(@user, project: @subject.project)
+ access = ::Gitlab::UserAccess.new(@user, container: @subject.project)
if @subject.tag?
!access.can_create_tag?(@subject.ref)
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index 662c29a0973..4d21da0226b 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -42,7 +42,7 @@ module Ci
end
def ref_protected?(user, project, tag, ref)
- access = ::Gitlab::UserAccess.new(user, project: project)
+ access = ::Gitlab::UserAccess.new(user, container: project)
if tag
!access.can_create_tag?(ref)
diff --git a/app/policies/concerns/crud_policy_helpers.rb b/app/policies/concerns/crud_policy_helpers.rb
index d8521ca22cc..029c196cc5f 100644
--- a/app/policies/concerns/crud_policy_helpers.rb
+++ b/app/policies/concerns/crud_policy_helpers.rb
@@ -13,10 +13,16 @@ module CrudPolicyHelpers
def create_update_admin_destroy(name)
[
+ *create_update_admin(name),
+ :"destroy_#{name}"
+ ]
+ end
+
+ def create_update_admin(name)
+ [
:"create_#{name}",
:"update_#{name}",
- :"admin_#{name}",
- :"destroy_#{name}"
+ :"admin_#{name}"
]
end
end
diff --git a/app/policies/concerns/readonly_abilities.rb b/app/policies/concerns/readonly_abilities.rb
new file mode 100644
index 00000000000..a267e963541
--- /dev/null
+++ b/app/policies/concerns/readonly_abilities.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module ReadonlyAbilities
+ extend ActiveSupport::Concern
+
+ READONLY_ABILITIES = %i[
+ admin_tag
+ push_code
+ push_to_delete_protected_branch
+ request_access
+ upload_file
+ resolve_note
+ create_merge_request_from
+ create_merge_request_in
+ award_emoji
+ ].freeze
+
+ READONLY_FEATURES = %i[
+ issue
+ list
+ merge_request
+ label
+ milestone
+ snippet
+ wiki
+ design
+ note
+ pipeline
+ pipeline_schedule
+ build
+ trigger
+ environment
+ deployment
+ commit_status
+ container_image
+ pages
+ cluster
+ release
+ ].freeze
+
+ class_methods do
+ def readonly_abilities
+ READONLY_ABILITIES
+ end
+
+ def readonly_features
+ READONLY_FEATURES
+ end
+ end
+end
+
+ReadonlyAbilities::ClassMethods.prepend_if_ee('EE::ReadonlyAbilities::ClassMethods')
diff --git a/app/policies/group_deploy_key_policy.rb b/app/policies/group_deploy_key_policy.rb
new file mode 100644
index 00000000000..642ed4d79ed
--- /dev/null
+++ b/app/policies/group_deploy_key_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class GroupDeployKeyPolicy < BasePolicy
+ with_options scope: :subject, score: 0
+ condition(:user_owns_group_deploy_key) { @subject.user_id == @user.id }
+
+ rule { user_owns_group_deploy_key }.enable :update_group_deploy_key
+end
diff --git a/app/policies/group_deploy_keys_group_policy.rb b/app/policies/group_deploy_keys_group_policy.rb
new file mode 100644
index 00000000000..9275d576923
--- /dev/null
+++ b/app/policies/group_deploy_keys_group_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class GroupDeployKeysGroupPolicy < BasePolicy
+ with_options scope: :subject, score: 0
+ delegate { @subject.group }
+ condition(:user_is_group_owner) { @subject.group.has_owner?(@user) }
+
+ rule { user_is_group_owner }.enable :update_group_deploy_key_for_group
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 92cba5f8f7d..3cc1be9dfb7 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -138,6 +138,7 @@ class GroupPolicy < BasePolicy
enable :read_group_labels
enable :read_group_milestones
enable :read_group_merge_requests
+ enable :read_group_build_report_results
end
rule { can?(:read_cross_project) & can?(:read_group) }.policy do
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 28baa0d8338..b02bb8621ed 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -35,8 +35,15 @@ class IssuePolicy < IssuablePolicy
prevent :destroy_design
end
+ rule { ~can?(:read_design) }.policy do
+ prevent :move_design
+ end
+
rule { locked | moved }.policy do
prevent :create_design
+ prevent :move_design
prevent :destroy_design
end
end
+
+IssuePolicy.prepend_if_ee('EE::IssuePolicy')
diff --git a/app/policies/personal_access_token_policy.rb b/app/policies/personal_access_token_policy.rb
new file mode 100644
index 00000000000..1e5404b7822
--- /dev/null
+++ b/app/policies/personal_access_token_policy.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class PersonalAccessTokenPolicy < BasePolicy
+ condition(:is_owner) { user && subject.user_id == user.id }
+
+ rule { (is_owner | admin) & ~blocked }.policy do
+ enable :read_token
+ enable :revoke_token
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 3a245119cb7..b2432bfa608 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -2,29 +2,7 @@
class ProjectPolicy < BasePolicy
include CrudPolicyHelpers
-
- READONLY_FEATURES_WHEN_ARCHIVED = %i[
- issue
- list
- merge_request
- label
- milestone
- snippet
- wiki
- design
- note
- pipeline
- pipeline_schedule
- build
- trigger
- environment
- deployment
- commit_status
- container_image
- pages
- cluster
- release
- ].freeze
+ include ReadonlyAbilities
desc "User is a project owner"
condition :owner do
@@ -124,6 +102,11 @@ class ProjectPolicy < BasePolicy
end
with_scope :subject
+ condition(:moving_designs_disabled) do
+ !::Feature.enabled?(:reorder_designs, @subject, default_enabled: true)
+ end
+
+ with_scope :subject
condition(:service_desk_enabled) { @subject.service_desk_enabled? }
# We aren't checking `:read_issue` or `:read_merge_request` in this case
@@ -248,6 +231,7 @@ class ProjectPolicy < BasePolicy
enable :admin_issue
enable :admin_label
enable :admin_list
+ enable :admin_issue_link
enable :read_commit_status
enable :read_build
enable :read_container_image
@@ -258,11 +242,13 @@ class ProjectPolicy < BasePolicy
enable :read_merge_request
enable :read_sentry_issue
enable :update_sentry_issue
+ enable :read_incidents
enable :read_prometheus
enable :read_metrics_dashboard_annotation
enable :metrics_dashboard
enable :read_confidential_issues
enable :read_package
+ enable :read_product_analytics
end
# We define `:public_user_access` separately because there are cases in gitlab-ee
@@ -340,8 +326,10 @@ class ProjectPolicy < BasePolicy
enable :read_alert_management_alert
enable :update_alert_management_alert
enable :create_design
+ enable :move_design
enable :destroy_design
enable :read_terraform_state
+ enable :read_pod_logs
end
rule { can?(:developer_access) & user_confirmed? }.policy do
@@ -381,7 +369,6 @@ class ProjectPolicy < BasePolicy
enable :admin_operations
enable :read_deploy_token
enable :create_deploy_token
- enable :read_pod_logs
enable :destroy_deploy_token
enable :read_prometheus_alerts
enable :admin_terraform_state
@@ -403,16 +390,9 @@ class ProjectPolicy < BasePolicy
rule { can?(:push_code) }.enable :admin_tag
rule { archived }.policy do
- prevent :push_code
- prevent :push_to_delete_protected_branch
- prevent :request_access
- prevent :upload_file
- prevent :resolve_note
- prevent :create_merge_request_from
- prevent :create_merge_request_in
- prevent :award_emoji
+ prevent(*readonly_abilities)
- READONLY_FEATURES_WHEN_ARCHIVED.each do |feature|
+ readonly_features.each do |feature|
prevent(*create_update_admin_destroy(feature))
end
end
@@ -499,6 +479,8 @@ class ProjectPolicy < BasePolicy
enable :read_note
enable :read_pipeline
enable :read_pipeline_schedule
+ enable :read_environment
+ enable :read_deployment
enable :read_commit_status
enable :read_container_image
enable :download_code
@@ -563,6 +545,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:read_issue) }.policy do
enable :read_design
enable :read_design_activity
+ enable :read_issue_link
end
# Design abilities could also be prevented in the issue policy.
@@ -571,6 +554,11 @@ class ProjectPolicy < BasePolicy
prevent :read_design_activity
prevent :create_design
prevent :destroy_design
+ prevent :move_design
+ end
+
+ rule { moving_designs_disabled }.policy do
+ prevent :move_design
end
rule { read_package_registry_deploy_token }.policy do
diff --git a/app/policies/prometheus_alert_policy.rb b/app/policies/prometheus_alert_policy.rb
new file mode 100644
index 00000000000..e6b0e6e8c17
--- /dev/null
+++ b/app/policies/prometheus_alert_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class PrometheusAlertPolicy < ::BasePolicy
+ delegate { @subject.project }
+end
diff --git a/app/policies/suggestion_policy.rb b/app/policies/suggestion_policy.rb
index 301b7d965f5..4c84c8ba690 100644
--- a/app/policies/suggestion_policy.rb
+++ b/app/policies/suggestion_policy.rb
@@ -4,7 +4,7 @@ class SuggestionPolicy < BasePolicy
delegate { @subject.project }
condition(:can_push_to_branch) do
- Gitlab::UserAccess.new(@user, project: @subject.project).can_push_to_branch?(@subject.branch)
+ Gitlab::UserAccess.new(@user, container: @subject.project).can_push_to_branch?(@subject.branch)
end
rule { can_push_to_branch }.enable :apply_suggestion
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 43f472b4c1d..6ebafca9885 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -20,6 +20,7 @@ class UserPolicy < BasePolicy
enable :destroy_user
enable :update_user
enable :update_user_status
+ enable :read_user_personal_access_tokens
end
rule { default }.enable :read_user_profile
diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb
index a515c70152d..5bfa6dee18b 100644
--- a/app/presenters/alert_management/alert_presenter.rb
+++ b/app/presenters/alert_management/alert_presenter.rb
@@ -4,6 +4,7 @@ module AlertManagement
class AlertPresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
include IncidentManagement::Settings
+ include ActionView::Helpers::UrlHelper
MARKDOWN_LINE_BREAK = " \n".freeze
@@ -37,8 +38,18 @@ module AlertManagement
MARKDOWN
end
+ def runbook
+ strong_memoize(:runbook) do
+ payload&.dig('runbook')
+ end
+ end
+
def metrics_dashboard_url; end
+ def details_url
+ details_project_alert_management_url(project, alert.iid)
+ end
+
private
attr_reader :alert, :project
@@ -61,6 +72,7 @@ module AlertManagement
metadata << list_item('Monitoring tool', monitoring_tool) if monitoring_tool
metadata << list_item('Hosts', host_links) if hosts.any?
metadata << list_item('Description', description) if description.present?
+ metadata << list_item('GitLab alert', details_url) if details_url.present?
metadata.join(MARKDOWN_LINE_BREAK)
end
diff --git a/app/presenters/alert_management/prometheus_alert_presenter.rb b/app/presenters/alert_management/prometheus_alert_presenter.rb
index 3bcc98e6784..6b8c8183f08 100644
--- a/app/presenters/alert_management/prometheus_alert_presenter.rb
+++ b/app/presenters/alert_management/prometheus_alert_presenter.rb
@@ -2,6 +2,12 @@
module AlertManagement
class PrometheusAlertPresenter < AlertManagement::AlertPresenter
+ def runbook
+ strong_memoize(:runbook) do
+ payload&.dig('annotations', 'runbook')
+ end
+ end
+
def metrics_dashboard_url
alerting_alert.metrics_dashboard_url
end
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index e0077db8d5c..cff935d51b5 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -18,6 +18,10 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
Gitlab::Routing.url_helpers.project_blob_url(blob.repository.project, File.join(blob.commit_id, blob.path))
end
+ def web_path
+ Gitlab::Routing.url_helpers.project_blob_path(blob.repository.project, File.join(blob.commit_id, blob.path))
+ end
+
private
def load_all_blob_data
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index 5e35bfc79ef..64461fa9193 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -118,3 +118,5 @@ module Ci
end
end
end
+
+Ci::BuildRunnerPresenter.prepend_if_ee('EE::Ci::BuildRunnerPresenter')
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index c0da5310ca4..25693af4881 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -80,7 +80,7 @@ module Clusters
'clusters-path': clusterable.index_path,
'dashboard-endpoint': clusterable.metrics_dashboard_path(cluster),
'documentation-path': help_page_path('user/project/clusters/index', anchor: 'monitoring-your-kubernetes-cluster-ultimate'),
- 'add-dashboard-documentation-path': help_page_path('user/project/integrations/prometheus.md', anchor: 'adding-a-new-dashboard-to-your-project'),
+ 'add-dashboard-documentation-path': help_page_path('operations/metrics/dashboards/index.md', anchor: 'add-a-new-dashboard-to-your-project'),
'empty-getting-started-svg-path': image_path('illustrations/monitoring/getting_started.svg'),
'empty-loading-svg-path': image_path('illustrations/monitoring/loading.svg'),
'empty-no-data-svg-path': image_path('illustrations/monitoring/no_data.svg'),
diff --git a/app/presenters/commit_presenter.rb b/app/presenters/commit_presenter.rb
index 9ded00fcb7a..c14dcab6000 100644
--- a/app/presenters/commit_presenter.rb
+++ b/app/presenters/commit_presenter.rb
@@ -17,10 +17,6 @@ class CommitPresenter < Gitlab::View::Presenter::Delegated
commit.pipelines.any?
end
- def web_url
- url_builder.build(commit)
- end
-
def signature_html
return unless commit.has_signature?
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index 52811e152a6..eaa7cf848cd 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -19,7 +19,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
downstream_bridge_project_not_found: 'This job could not be executed because downstream bridge project could not be found',
insufficient_bridge_permissions: 'This job could not be executed because of insufficient permissions to create a downstream pipeline',
bridge_pipeline_is_child_pipeline: 'This job belongs to a child pipeline and cannot create further child pipelines',
- downstream_pipeline_creation_failed: 'The downstream pipeline could not be created'
+ downstream_pipeline_creation_failed: 'The downstream pipeline could not be created',
+ secrets_provider_not_found: 'The secrets provider can not be found'
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb
index 5657e0b96bc..8f2388c2c31 100644
--- a/app/presenters/event_presenter.rb
+++ b/app/presenters/event_presenter.rb
@@ -24,7 +24,7 @@ class EventPresenter < Gitlab::View::Presenter::Delegated
when Group
[event.group, event.target]
when Project
- [event.project.namespace.becomes(Namespace), event.project, event.target]
+ [event.project, event.target]
else
''
end
diff --git a/app/presenters/gitlab/blame_presenter.rb b/app/presenters/gitlab/blame_presenter.rb
index db2fc52a88b..3c581d4b115 100644
--- a/app/presenters/gitlab/blame_presenter.rb
+++ b/app/presenters/gitlab/blame_presenter.rb
@@ -76,7 +76,7 @@ module Gitlab
end
def versions_sprite_icon
- @versions_sprite_icon ||= sprite_icon('doc-versions', size: 16, css_class: 'doc-versions align-text-bottom')
+ @versions_sprite_icon ||= sprite_icon('doc-versions', css_class: 'doc-versions align-text-bottom')
end
end
end
diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb
index 004813d0374..185fcd3e934 100644
--- a/app/presenters/issue_presenter.rb
+++ b/app/presenters/issue_presenter.rb
@@ -3,10 +3,6 @@
class IssuePresenter < Gitlab::View::Presenter::Delegated
presents :issue
- def web_url
- url_builder.build(issue)
- end
-
def issue_path
url_builder.build(issue, only_path: true)
end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index bccf0340749..1ff02412994 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -179,7 +179,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
return false unless source_branch_exists?
!!::Gitlab::UserAccess
- .new(current_user, project: source_project)
+ .new(current_user, container: source_project)
.can_push_to_branch?(source_branch)
end
@@ -202,10 +202,6 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
- def web_url
- Gitlab::UrlBuilder.build(merge_request)
- end
-
def subscribed?
merge_request.subscribed?(current_user, merge_request.target_project)
end
diff --git a/app/presenters/packages/detail/package_presenter.rb b/app/presenters/packages/detail/package_presenter.rb
index f6e068302c1..bdb2e34854e 100644
--- a/app/presenters/packages/detail/package_presenter.rb
+++ b/app/presenters/packages/detail/package_presenter.rb
@@ -22,6 +22,8 @@ module Packages
package_detail[:maven_metadatum] = @package.maven_metadatum if @package.maven_metadatum
package_detail[:nuget_metadatum] = @package.nuget_metadatum if @package.nuget_metadatum
+ package_detail[:composer_metadatum] = @package.composer_metadatum if @package.composer_metadatum
+ package_detail[:conan_metadatum] = @package.conan_metadatum if @package.conan_metadatum
package_detail[:dependency_links] = @package.dependency_links.map(&method(:build_dependency_links))
package_detail[:pipeline] = build_pipeline_info(@package.build_info.pipeline) if @package.build_info
@@ -49,7 +51,9 @@ module Packages
user: build_user_info(pipeline_info.user),
project: {
name: pipeline_info.project.name,
- web_url: pipeline_info.project.web_url
+ web_url: pipeline_info.project.web_url,
+ pipeline_url: Gitlab::Routing.url_helpers.project_pipeline_url(pipeline_info.project, pipeline_info),
+ commit_url: Gitlab::Routing.url_helpers.project_commit_url(pipeline_info.project, pipeline_info.sha)
}
}
end
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 4e8dae1d508..86fd405812e 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -16,7 +16,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
MAX_TOPICS_TO_SHOW = 3
def statistic_icon(icon_name = 'plus-square-o')
- sprite_icon(icon_name, size: 16, css_class: 'icon gl-mr-2')
+ sprite_icon(icon_name, css_class: 'icon gl-mr-2')
end
def statistics_anchors(show_auto_devops_callout:)
diff --git a/app/presenters/projects/prometheus/alert_presenter.rb b/app/presenters/projects/prometheus/alert_presenter.rb
index 1cf8b202810..49859f27edd 100644
--- a/app/presenters/projects/prometheus/alert_presenter.rb
+++ b/app/presenters/projects/prometheus/alert_presenter.rb
@@ -77,6 +77,15 @@ module Projects
end
end
+ def details_url
+ return unless am_alert
+
+ ::Gitlab::Routing.url_helpers.details_project_alert_management_url(
+ project,
+ am_alert.iid
+ )
+ end
+
private
def alert_title
@@ -97,6 +106,7 @@ module Projects
metadata << list_item(service.label.humanize, service.value) if service
metadata << list_item(monitoring_tool.label.humanize, monitoring_tool.value) if monitoring_tool
metadata << list_item(hosts.label.humanize, host_links) if hosts
+ metadata << list_item('GitLab alert', details_url) if details_url
metadata.join(MARKDOWN_LINE_BREAK)
end
@@ -173,7 +183,7 @@ module Projects
{
panel_groups: [{
panels: [{
- type: 'line-graph',
+ type: 'area-chart',
title: title,
y_label: y_label,
metrics: [{
diff --git a/app/presenters/prometheus_alert_presenter.rb b/app/presenters/prometheus_alert_presenter.rb
new file mode 100644
index 00000000000..99e24bdcdb9
--- /dev/null
+++ b/app/presenters/prometheus_alert_presenter.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class PrometheusAlertPresenter < Gitlab::View::Presenter::Delegated
+ include ActionView::Helpers::UrlHelper
+
+ presents :prometheus_alert
+
+ def humanized_text
+ operator_text =
+ case prometheus_alert.operator
+ when 'lt' then s_('PrometheusAlerts|is less than')
+ when 'eq' then s_('PrometheusAlerts|is equal to')
+ when 'gt' then s_('PrometheusAlerts|exceeded')
+ end
+
+ "#{operator_text} #{prometheus_alert.threshold}#{prometheus_alert.prometheus_metric.unit}"
+ end
+end
diff --git a/app/presenters/snippet_blob_presenter.rb b/app/presenters/snippet_blob_presenter.rb
index d27fe751ab7..abe95f5c44d 100644
--- a/app/presenters/snippet_blob_presenter.rb
+++ b/app/presenters/snippet_blob_presenter.rb
@@ -4,7 +4,6 @@ class SnippetBlobPresenter < BlobPresenter
include GitlabRoutingHelper
def rich_data
- return if blob.binary?
return unless blob.rich_viewer
render_rich_partial
@@ -17,9 +16,11 @@ class SnippetBlobPresenter < BlobPresenter
end
def raw_path
- return gitlab_raw_snippet_blob_path(blob) if snippet_multiple_files?
+ snippet_blob_raw_route(only_path: true)
+ end
- gitlab_raw_snippet_path(snippet)
+ def raw_url
+ snippet_blob_raw_route
end
private
@@ -38,7 +39,7 @@ class SnippetBlobPresenter < BlobPresenter
def render_rich_partial
renderer.render("projects/blob/viewers/_#{blob.rich_viewer.partial_name}",
- locals: { viewer: blob.rich_viewer, blob: blob, blob_raw_path: raw_path },
+ locals: { viewer: blob.rich_viewer, blob: blob, blob_raw_path: raw_path, blob_raw_url: raw_url },
layout: false)
end
@@ -49,4 +50,10 @@ class SnippetBlobPresenter < BlobPresenter
ApplicationController.renderer.new('warden' => proxy)
end
+
+ def snippet_blob_raw_route(only_path: false)
+ return gitlab_raw_snippet_blob_url(snippet, blob.path, only_path: only_path) if snippet_multiple_files?
+
+ gitlab_raw_snippet_url(snippet, only_path: only_path)
+ end
end
diff --git a/app/presenters/snippet_presenter.rb b/app/presenters/snippet_presenter.rb
index 62a90025ce1..d814c4404b6 100644
--- a/app/presenters/snippet_presenter.rb
+++ b/app/presenters/snippet_presenter.rb
@@ -3,12 +3,8 @@
class SnippetPresenter < Gitlab::View::Presenter::Delegated
presents :snippet
- def web_url
- Gitlab::UrlBuilder.build(snippet)
- end
-
def raw_url
- Gitlab::UrlBuilder.build(snippet, raw: true)
+ url_builder.build(snippet, raw: true)
end
def ssh_url_to_repo
diff --git a/app/presenters/tree_entry_presenter.rb b/app/presenters/tree_entry_presenter.rb
index 7bb10cd1455..216b3b0d4c9 100644
--- a/app/presenters/tree_entry_presenter.rb
+++ b/app/presenters/tree_entry_presenter.rb
@@ -6,4 +6,8 @@ class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
def web_url
Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path))
end
+
+ def web_path
+ Gitlab::Routing.url_helpers.project_tree_path(tree.repository.project, File.join(tree.commit_id, tree.path))
+ end
end
diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb
index 14ef53e9ec8..f201b36346f 100644
--- a/app/presenters/user_presenter.rb
+++ b/app/presenters/user_presenter.rb
@@ -2,8 +2,4 @@
class UserPresenter < Gitlab::View::Presenter::Delegated
presents :user
-
- def web_url
- Gitlab::Routing.url_helpers.user_url(user)
- end
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index df1bdc2b7a4..523f1a0f8c6 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -27,11 +27,11 @@ class BuildDetailsEntity < JobEntity
end
expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do
- expose :download_path, if: -> (*) { build.artifacts? } do |build|
+ expose :download_path, if: -> (*) { build.pipeline.artifacts_locked? || build.artifacts? } do |build|
download_project_job_artifacts_path(project, build)
end
- expose :browse_path, if: -> (*) { build.browsable_artifacts? } do |build|
+ expose :browse_path, if: -> (*) { build.pipeline.artifacts_locked? || build.browsable_artifacts? } do |build|
browse_project_job_artifacts_path(project, build)
end
@@ -46,6 +46,10 @@ class BuildDetailsEntity < JobEntity
expose :expired, if: -> (*) { build.artifacts_expire_at.present? } do |build|
build.artifacts_expired?
end
+
+ expose :locked do |build|
+ build.pipeline.artifacts_locked?
+ end
end
expose :report_artifacts,
@@ -147,7 +151,7 @@ class BuildDetailsEntity < JobEntity
end
def help_message(docs_url)
- _("Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>") % { docs_url: docs_url }
+ html_escape(_("Please refer to %{docs_url}")) % { docs_url: "<a href=\"#{docs_url}\">#{html_escape(docs_url)}</a>".html_safe }
end
end
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
index a46f2889a96..06e14179238 100644
--- a/app/serializers/cluster_entity.rb
+++ b/app/serializers/cluster_entity.rb
@@ -20,4 +20,8 @@ class ClusterEntity < Grape::Entity
expose :gitlab_managed_apps_logs_path do |cluster|
Clusters::ClusterPresenter.new(cluster, current_user: request.current_user).gitlab_managed_apps_logs_path # rubocop: disable CodeReuse/Presenter
end
+
+ expose :kubernetes_errors do |cluster|
+ ClusterErrorEntity.new(cluster)
+ end
end
diff --git a/app/serializers/cluster_error_entity.rb b/app/serializers/cluster_error_entity.rb
new file mode 100644
index 00000000000..c749537cb94
--- /dev/null
+++ b/app/serializers/cluster_error_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class ClusterErrorEntity < Grape::Entity
+ expose :connection_error
+ expose :metrics_connection_error
+ expose :node_connection_error
+end
diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb
index 92363a4942c..a70458d2bcb 100644
--- a/app/serializers/cluster_serializer.rb
+++ b/app/serializers/cluster_serializer.rb
@@ -11,6 +11,7 @@ class ClusterSerializer < BaseSerializer
:enabled,
:environment_scope,
:gitlab_managed_apps_logs_path,
+ :kubernetes_errors,
:name,
:nodes,
:path,
diff --git a/app/serializers/diffs_metadata_entity.rb b/app/serializers/diffs_metadata_entity.rb
index b7024721ea9..8973f23734a 100644
--- a/app/serializers/diffs_metadata_entity.rb
+++ b/app/serializers/diffs_metadata_entity.rb
@@ -3,4 +3,23 @@
class DiffsMetadataEntity < DiffsEntity
unexpose :diff_files
expose :raw_diff_files, as: :diff_files, using: DiffFileMetadataEntity
+
+ expose :conflict_resolution_path do |_, options|
+ presenter(options[:merge_request]).conflict_resolution_path
+ end
+
+ expose :has_conflicts do |_, options|
+ options[:merge_request].cannot_be_merged?
+ end
+
+ expose :can_merge do |_, options|
+ options[:merge_request].can_be_merged_by?(request.current_user)
+ end
+
+ private
+
+ def presenter(merge_request)
+ @presenters ||= {}
+ @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: request.current_user) # rubocop: disable CodeReuse/Presenter
+ end
end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index 77881eaba0c..2957205a81c 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -72,6 +72,6 @@ class DiscussionEntity < Grape::Entity
return unless discussion.diff_discussion?
return if discussion.legacy_diff_discussion?
- Feature.enabled?(:merge_ref_head_comments, discussion.project)
+ Feature.enabled?(:merge_ref_head_comments, discussion.project, default_enabled: true)
end
end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 7da5910a75b..a2bf9716f8f 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -71,6 +71,8 @@ class EnvironmentEntity < Grape::Entity
can?(current_user, :destroy_environment, environment)
end
+ expose :has_opened_alert?, if: -> (*) { can_read_alert_management_alert? }, expose_nil: false, as: :has_opened_alert
+
private
alias_method :environment, :object
@@ -91,6 +93,10 @@ class EnvironmentEntity < Grape::Entity
can?(current_user, :read_pod_logs, environment.project)
end
+ def can_read_alert_management_alert?
+ can?(current_user, :read_alert_management_alert, environment.project)
+ end
+
def cluster_platform_kubernetes?
deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
end
diff --git a/app/serializers/group_basic_entity.rb b/app/serializers/group_basic_entity.rb
new file mode 100644
index 00000000000..24a05100d43
--- /dev/null
+++ b/app/serializers/group_basic_entity.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class GroupBasicEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :name
+ expose :full_path
+ expose :full_name
+end
diff --git a/app/serializers/group_deploy_key_entity.rb b/app/serializers/group_deploy_key_entity.rb
new file mode 100644
index 00000000000..c0bb0448a51
--- /dev/null
+++ b/app/serializers/group_deploy_key_entity.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class GroupDeployKeyEntity < Grape::Entity
+ expose :id
+ expose :user_id
+ expose :title
+ expose :fingerprint
+ expose :fingerprint_sha256
+ expose :created_at
+ expose :updated_at
+ expose :group_deploy_keys_groups, using: GroupDeployKeysGroupEntity do |group_deploy_key|
+ group_deploy_key.group_deploy_keys_groups_for_user(options[:user])
+ end
+ expose :can_edit do |group_deploy_key|
+ group_deploy_key.can_be_edited_for?(options[:user], options[:group])
+ end
+end
diff --git a/app/serializers/group_deploy_key_serializer.rb b/app/serializers/group_deploy_key_serializer.rb
new file mode 100644
index 00000000000..e7d5f6a77ea
--- /dev/null
+++ b/app/serializers/group_deploy_key_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class GroupDeployKeySerializer < BaseSerializer
+ entity GroupDeployKeyEntity
+end
diff --git a/app/serializers/group_deploy_keys_group_entity.rb b/app/serializers/group_deploy_keys_group_entity.rb
new file mode 100644
index 00000000000..f2801dfc112
--- /dev/null
+++ b/app/serializers/group_deploy_keys_group_entity.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class GroupDeployKeysGroupEntity < Grape::Entity
+ expose :can_push
+ expose :group, using: GroupBasicEntity
+end
diff --git a/app/serializers/import/bitbucket_server_provider_repo_entity.rb b/app/serializers/import/bitbucket_server_provider_repo_entity.rb
index d818cac46cd..7c619cf4ebe 100644
--- a/app/serializers/import/bitbucket_server_provider_repo_entity.rb
+++ b/app/serializers/import/bitbucket_server_provider_repo_entity.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class Import::BitbucketServerProviderRepoEntity < Import::BitbucketProviderRepoEntity
+ expose :id, override: true do |repo|
+ "#{repo.project_key}/#{repo.slug}"
+ end
+
expose :provider_link, override: true do |repo, options|
repo.browse_url
end
diff --git a/app/serializers/import/manifest_provider_repo_entity.rb b/app/serializers/import/manifest_provider_repo_entity.rb
new file mode 100644
index 00000000000..5da9aae80a8
--- /dev/null
+++ b/app/serializers/import/manifest_provider_repo_entity.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class Import::ManifestProviderRepoEntity < Import::BaseProviderRepoEntity
+ expose :id
+ expose :full_name, override: true do |repo|
+ repo[:url]
+ end
+
+ expose :provider_link, override: true do |repo|
+ repo[:url]
+ end
+
+ expose :target do |repo, options|
+ import_project_target(options[:group_full_path], repo[:path], options[:request].current_user)
+ end
+
+ private
+
+ def import_project_target(owner, name, user)
+ namespace = user.can_create_group? ? owner : user.namespace_path
+ "#{namespace}/#{name}"
+ end
+end
diff --git a/app/serializers/import/provider_repo_serializer.rb b/app/serializers/import/provider_repo_serializer.rb
index 5a9549d79aa..edd1a260146 100644
--- a/app/serializers/import/provider_repo_serializer.rb
+++ b/app/serializers/import/provider_repo_serializer.rb
@@ -14,6 +14,8 @@ class Import::ProviderRepoSerializer < BaseSerializer
Import::BitbucketServerProviderRepoEntity
when :gitlab
Import::GitlabProviderRepoEntity
+ when :manifest
+ Import::ManifestProviderRepoEntity
else
raise NotImplementedError
end
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index a365ebc29c9..99d6211b487 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -19,9 +19,21 @@ class MergeRequestPollWidgetEntity < Grape::Entity
# User entities
expose :merge_user, using: UserEntity
- expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline, if: -> (mr, _) { presenter(mr).can_read_pipeline? }
+ expose :actual_head_pipeline, as: :pipeline, if: -> (mr, _) { presenter(mr).can_read_pipeline? } do |merge_request, options|
+ if Feature.enabled?(:merge_request_short_pipeline_serializer, merge_request.project, default_enabled: true)
+ MergeRequests::PipelineEntity.represent(merge_request.actual_head_pipeline, options)
+ else
+ PipelineDetailsEntity.represent(merge_request.actual_head_pipeline, options)
+ end
+ end
- expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)}
+ expose :merge_pipeline, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)} do |merge_request, options|
+ if Feature.enabled?(:merge_request_short_pipeline_serializer, merge_request.project, default_enabled: true)
+ MergeRequests::PipelineEntity.represent(merge_request.merge_pipeline, options)
+ else
+ PipelineDetailsEntity.represent(merge_request.merge_pipeline, options)
+ end
+ end
expose :default_merge_commit_message
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 2a7afb57314..b7b9e7d1036 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -3,6 +3,8 @@
class MergeRequestWidgetEntity < Grape::Entity
include RequestAwareEntity
+ SUGGEST_PIPELINE = 'suggest_pipeline'
+
expose :id
expose :iid
@@ -14,6 +16,10 @@ class MergeRequestWidgetEntity < Grape::Entity
merge_request.project&.full_path
end
+ expose :can_create_pipeline_in_target_project do |merge_request|
+ can?(current_user, :create_pipeline, merge_request.target_project)
+ end
+
expose :email_patches_path do |merge_request|
project_merge_request_path(merge_request.project, merge_request, format: :patch)
end
@@ -60,6 +66,18 @@ class MergeRequestWidgetEntity < Grape::Entity
)
end
+ expose :user_callouts_path, if: -> (*) { Feature.enabled?(:suggest_pipeline) } do |merge_request|
+ user_callouts_path
+ end
+
+ expose :suggest_pipeline_feature_id, if: -> (*) { Feature.enabled?(:suggest_pipeline) } do |merge_request|
+ SUGGEST_PIPELINE
+ end
+
+ expose :is_dismissed_suggest_pipeline, if: -> (*) { Feature.enabled?(:suggest_pipeline) } do |merge_request|
+ current_user && current_user.dismissed_callout?(feature_name: SUGGEST_PIPELINE)
+ end
+
expose :human_access do |merge_request|
merge_request.project.team.human_max_access(current_user&.id)
end
@@ -119,7 +137,7 @@ class MergeRequestWidgetEntity < Grape::Entity
merge_request.source_branch_exists? &&
merge_request.source_project&.uses_default_ci_config? &&
!merge_request.source_project.has_ci? &&
- merge_request.commits_count.positive? &&
+ merge_request.commits_count > 0 &&
can?(current_user, :read_build, merge_request.source_project) &&
can?(current_user, :create_pipeline, merge_request.source_project)
end
diff --git a/app/serializers/merge_requests/pipeline_entity.rb b/app/serializers/merge_requests/pipeline_entity.rb
new file mode 100644
index 00000000000..97d7620154e
--- /dev/null
+++ b/app/serializers/merge_requests/pipeline_entity.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+class MergeRequests::PipelineEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id
+ expose :active?, as: :active
+
+ expose :path do |pipeline|
+ project_pipeline_path(pipeline.project, pipeline)
+ end
+
+ expose :flags do
+ expose :merge_request_pipeline?, as: :merge_request_pipeline
+ end
+
+ expose :commit, using: CommitEntity
+
+ expose :details do
+ expose :name do |pipeline|
+ pipeline.present.name
+ end
+
+ expose :detailed_status, as: :status, with: DetailedStatusEntity do |pipeline|
+ pipeline.detailed_status(request.current_user)
+ end
+
+ expose :stages, using: StageEntity
+ end
+
+ # Coverage isn't always necessary (e.g. when displaying project pipelines in
+ # the UI). Instead of creating an entirely different entity we just allow the
+ # disabling of this specific field whenever necessary.
+ expose :coverage, unless: proc { options[:disable_coverage] }
+
+ expose :ref do
+ expose :branch?, as: :branch
+ end
+
+ expose :triggered_by_pipeline, as: :triggered_by, with: TriggeredPipelineEntity
+ expose :triggered_pipelines, as: :triggered, using: TriggeredPipelineEntity
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 8333a0bb863..de1e07139ad 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -36,7 +36,7 @@ class PipelineEntity < Grape::Entity
expose :details do
expose :detailed_status, as: :status, with: DetailedStatusEntity
- expose :ordered_stages, as: :stages, using: StageEntity
+ expose :stages, using: StageEntity
expose :duration
expose :finished_at
expose :name
@@ -85,8 +85,8 @@ class PipelineEntity < Grape::Entity
pipeline.failed_builds
end
- expose :tests_total_count, if: -> (pipeline, _) { Feature.enabled?(:build_report_summary, pipeline.project) } do |pipeline|
- pipeline.test_report_summary.total_count
+ expose :tests_total_count do |pipeline|
+ pipeline.test_report_summary.total[:count]
end
private
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index bfd6851647f..45c5a1d3e1c 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -42,6 +42,7 @@ class PipelineSerializer < BaseSerializer
[
:cancelable_statuses,
:latest_statuses_ordered_by_stage,
+ :latest_builds_report_results,
:manual_actions,
:retryable_builds,
:scheduled_actions,
diff --git a/app/serializers/prometheus_alert_entity.rb b/app/serializers/prometheus_alert_entity.rb
index 413be511903..92905d2b389 100644
--- a/app/serializers/prometheus_alert_entity.rb
+++ b/app/serializers/prometheus_alert_entity.rb
@@ -7,6 +7,7 @@ class PrometheusAlertEntity < Grape::Entity
expose :title
expose :query
expose :threshold
+ expose :runbook_url
expose :operator do |prometheus_alert|
prometheus_alert.computed_operator
diff --git a/app/serializers/release_entity.rb b/app/serializers/release_entity.rb
new file mode 100644
index 00000000000..6777b0f9780
--- /dev/null
+++ b/app/serializers/release_entity.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class ReleaseEntity < Grape::Entity
+ expose :id
+ expose :tag # see https://gitlab.com/gitlab-org/gitlab/-/issues/36338
+end
diff --git a/app/serializers/release_serializer.rb b/app/serializers/release_serializer.rb
new file mode 100644
index 00000000000..05a13f71a6f
--- /dev/null
+++ b/app/serializers/release_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ReleaseSerializer < BaseSerializer
+ entity ReleaseEntity
+end
diff --git a/app/serializers/suggestion_entity.rb b/app/serializers/suggestion_entity.rb
index c9fcbe14f2e..c224d0b4390 100644
--- a/app/serializers/suggestion_entity.rb
+++ b/app/serializers/suggestion_entity.rb
@@ -16,24 +16,8 @@ class SuggestionEntity < API::Entities::Suggestion
expose :inapplicable_reason do |suggestion|
next _("You don't have write access to the source branch.") unless can_apply?(suggestion)
- next if suggestion.appliable?
- case suggestion.inapplicable_reason
- when :merge_request_merged
- _("This merge request was merged. To apply this suggestion, edit this file directly.")
- when :merge_request_closed
- _("This merge request is closed. To apply this suggestion, edit this file directly.")
- when :source_branch_deleted
- _("Can't apply as the source branch was deleted.")
- when :outdated
- phrase = suggestion.single_line? ? 'this line was' : 'these lines were'
-
- _("Can't apply as %{phrase} changed in a more recent version.") % { phrase: phrase }
- when :same_content
- _("This suggestion already matches its content.")
- else
- _("Can't apply this suggestion.")
- end
+ suggestion.inapplicable_reason
end
private
diff --git a/app/serializers/test_report_summary_entity.rb b/app/serializers/test_report_summary_entity.rb
index 5995ca007d6..bc73c49092f 100644
--- a/app/serializers/test_report_summary_entity.rb
+++ b/app/serializers/test_report_summary_entity.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-class TestReportSummaryEntity < TestReportEntity
+class TestReportSummaryEntity < Grape::Entity
+ expose :total
+
expose :test_suites, using: TestSuiteSummaryEntity do |summary|
summary.test_suites.values
end
diff --git a/app/serializers/triggered_pipeline_entity.rb b/app/serializers/triggered_pipeline_entity.rb
index 47f51a6d76a..9fdadb322bf 100644
--- a/app/serializers/triggered_pipeline_entity.rb
+++ b/app/serializers/triggered_pipeline_entity.rb
@@ -24,8 +24,8 @@ class TriggeredPipelineEntity < Grape::Entity
expose :details do
expose :detailed_status, as: :status, with: DetailedStatusEntity
- expose :ordered_stages,
- as: :stages, using: StageEntity,
+ expose :stages,
+ using: StageEntity,
if: -> (_, opts) { can_read_details? && expand?(opts) }
end
diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb
index e21bb03ed68..9a5ce58ee2c 100644
--- a/app/services/admin/propagate_integration_service.rb
+++ b/app/services/admin/propagate_integration_service.rb
@@ -96,7 +96,7 @@ module Admin
# rubocop: disable CodeReuse/ActiveRecord
def run_callbacks(batch)
- if active_external_issue_tracker?
+ if integration.issue_tracker?
Project.where(id: batch).update_all(has_external_issue_tracker: true)
end
@@ -106,10 +106,6 @@ module Admin
end
# rubocop: enable CodeReuse/ActiveRecord
- def active_external_issue_tracker?
- integration.issue_tracker? && !integration.default
- end
-
def active_external_wiki?
integration.type == 'ExternalWikiService'
end
diff --git a/app/services/alert_management/alerts/update_service.rb b/app/services/alert_management/alerts/update_service.rb
index 0b7216cd9f8..18d615aa7e7 100644
--- a/app/services/alert_management/alerts/update_service.rb
+++ b/app/services/alert_management/alerts/update_service.rb
@@ -96,12 +96,12 @@ module AlertManagement
end
def handle_assignement(old_assignees)
- assign_todo
+ assign_todo(old_assignees)
add_assignee_system_note(old_assignees)
end
- def assign_todo
- todo_service.assign_alert(alert, current_user)
+ def assign_todo(old_assignees)
+ todo_service.reassigned_assignable(alert, current_user, old_assignees)
end
def add_assignee_system_note(old_assignees)
diff --git a/app/services/alert_management/create_alert_issue_service.rb b/app/services/alert_management/create_alert_issue_service.rb
index 6ea3fd867ef..f16b106b748 100644
--- a/app/services/alert_management/create_alert_issue_service.rb
+++ b/app/services/alert_management/create_alert_issue_service.rb
@@ -15,10 +15,10 @@ module AlertManagement
return error_no_permissions unless allowed?
return error_issue_already_exists if alert.issue
- result = create_issue
- issue = result.payload[:issue]
+ result = create_incident
+ return result unless result.success?
- return error(result.message, issue) if result.error?
+ issue = result.payload[:issue]
return error(object_errors(alert), issue) unless associate_alert_with_issue(issue)
SystemNoteService.new_alert_issue(alert, issue, user)
@@ -36,35 +36,19 @@ module AlertManagement
user.can?(:create_issue, project)
end
- def create_issue
- label_result = find_or_create_incident_label
-
- # Create an unlabelled issue if we couldn't create the label
- # due to a race condition.
- # See https://gitlab.com/gitlab-org/gitlab-foss/issues/65042
- extra_params = label_result.success? ? { label_ids: [label_result.payload[:label].id] } : {}
-
- issue = Issues::CreateService.new(
+ def create_incident
+ ::IncidentManagement::Incidents::CreateService.new(
project,
user,
title: alert_presenter.title,
- description: alert_presenter.issue_description,
- **extra_params
+ description: alert_presenter.issue_description
).execute
-
- return error(object_errors(issue), issue) unless issue.valid?
-
- success(issue)
end
def associate_alert_with_issue(issue)
alert.update(issue_id: issue.id)
end
- def success(issue)
- ServiceResponse.success(payload: { issue: issue })
- end
-
def error(message, issue = nil)
ServiceResponse.error(payload: { issue: issue }, message: message)
end
@@ -83,10 +67,6 @@ module AlertManagement
end
end
- def find_or_create_incident_label
- IncidentManagement::CreateIncidentLabelService.new(project, user).execute
- end
-
def object_errors(object)
object.errors.full_messages.to_sentence
end
diff --git a/app/services/award_emojis/copy_service.rb b/app/services/award_emojis/copy_service.rb
new file mode 100644
index 00000000000..2e500d4c697
--- /dev/null
+++ b/app/services/award_emojis/copy_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# This service copies AwardEmoji from one Awardable to another.
+#
+# It expects the calling code to have performed the necessary authorization
+# checks in order to allow the copy to happen.
+module AwardEmojis
+ class CopyService
+ def initialize(from_awardable, to_awardable)
+ raise ArgumentError, 'Awardables must be different' if from_awardable == to_awardable
+
+ @from_awardable = from_awardable
+ @to_awardable = to_awardable
+ end
+
+ def execute
+ from_awardable.award_emoji.find_each do |award|
+ new_award = award.dup
+ new_award.awardable = to_awardable
+ new_award.save!
+ end
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_accessor :from_awardable, :to_awardable
+ end
+end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index e08509b84db..140420a32bd 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -45,6 +45,12 @@ module Boards
# rubocop: enable CodeReuse/ActiveRecord
def filter(issues)
+ # when grouping board issues by epics (used in board swimlanes)
+ # we need to get all issues in the board
+ # TODO: ignore hidden columns -
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/233870
+ return issues if params[:all_lists]
+
issues = without_board_labels(issues) unless list&.movable? || list&.closed?
issues = with_list_label(issues) if list&.label?
issues
@@ -55,9 +61,17 @@ module Boards
end
def list
- return @list if defined?(@list)
+ return unless params.key?(:id)
+
+ strong_memoize(:list) do
+ id = params[:id]
- @list = board.lists.find(params[:id]) if params.key?(:id)
+ if board.lists.loaded?
+ board.lists.find { |l| l.id == id }
+ else
+ board.lists.find(id)
+ end
+ end
end
def filter_params
@@ -79,6 +93,8 @@ module Boards
end
def set_state
+ return if params[:all_lists]
+
params[:state] = list && list.closed? ? 'closed' : 'opened'
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 9e3c84d03ec..14e8683ebdf 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -130,7 +130,7 @@ module Boards
def move_between_ids(move_params)
ids = [move_params[:move_after_id], move_params[:move_before_id]]
.map(&:to_i)
- .map { |m| m.positive? ? m : nil }
+ .map { |m| m > 0 ? m : nil }
ids.any? ? ids : nil
end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index 6f9a063cb16..9c7a165776e 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -7,34 +7,39 @@ module Boards
def execute(board)
List.transaction do
- target = target(board)
- position = next_position(board)
- create_list(board, type, target, position)
+ case type
+ when :backlog
+ create_backlog(board)
+ else
+ target = target(board)
+ position = next_position(board)
+
+ create_list(board, type, target, position)
+ end
end
end
private
def type
- :label
+ # We don't ever expect to have more than one list
+ # type param at once.
+ if params.key?('backlog')
+ :backlog
+ else
+ :label
+ end
end
def target(board)
strong_memoize(:target) do
- available_labels_for(board).find(params[:label_id])
+ available_labels.find(params[:label_id])
end
end
- def available_labels_for(board)
- options = { include_ancestor_groups: true }
-
- if board.group_board?
- options.merge!(group_id: parent.id, only_group_labels: true)
- else
- options[:project_id] = parent.id
- end
-
- LabelsFinder.new(current_user, options).execute
+ def available_labels
+ ::Labels::AvailableLabelsService.new(current_user, parent, {})
+ .available_labels
end
def next_position(board)
@@ -49,6 +54,12 @@ module Boards
def create_list_attributes(type, target, position)
{ type => target, list_type: type, position: position }
end
+
+ def create_backlog(board)
+ return board.lists.backlog.first if board.lists.backlog.exists?
+
+ board.lists.create(list_type: :backlog, position: nil)
+ end
end
end
end
diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb
index 07ce58b6851..e4c789c4597 100644
--- a/app/services/boards/lists/list_service.rb
+++ b/app/services/boards/lists/list_service.rb
@@ -8,7 +8,8 @@ module Boards
board.lists.create(list_type: :backlog)
end
- board.lists.preload_associated_models
+ lists = board.lists.preload_associated_models
+ params[:list_id].present? ? lists.where(id: params[:list_id]) : lists # rubocop: disable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/services/branches/create_service.rb b/app/services/branches/create_service.rb
index 958dd5c9965..8684da701db 100644
--- a/app/services/branches/create_service.rb
+++ b/app/services/branches/create_service.rb
@@ -16,8 +16,9 @@ module Branches
else
error("Invalid reference name: #{ref}")
end
- rescue Gitlab::Git::PreReceiveError => ex
- error(ex.message)
+ rescue Gitlab::Git::PreReceiveError => e
+ Gitlab::ErrorTracking.track_exception(e, pre_receive_message: e.raw_message, branch_name: branch_name, ref: ref)
+ error(e.message)
end
def success(branch)
diff --git a/app/services/ci/build_report_result_service.rb b/app/services/ci/build_report_result_service.rb
index 758ba1c73bf..ca66ad8249d 100644
--- a/app/services/ci/build_report_result_service.rb
+++ b/app/services/ci/build_report_result_service.rb
@@ -3,7 +3,6 @@
module Ci
class BuildReportResultService
def execute(build)
- return unless Feature.enabled?(:build_report_summary, build.project)
return unless build.has_test_reports?
build.report_results.create!(
diff --git a/app/services/ci/change_variable_service.rb b/app/services/ci/change_variable_service.rb
new file mode 100644
index 00000000000..f515a335d54
--- /dev/null
+++ b/app/services/ci/change_variable_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Ci
+ class ChangeVariableService < BaseContainerService
+ def execute
+ case params[:action]
+ when :create
+ container.variables.create(params[:variable_params])
+ when :update
+ variable.tap do |target_variable|
+ target_variable.update(params[:variable_params].except(:key))
+ end
+ when :destroy
+ variable.tap do |target_variable|
+ target_variable.destroy
+ end
+ end
+ end
+
+ private
+
+ def variable
+ params[:variable] || find_variable
+ end
+
+ def find_variable
+ identifier = params[:variable_params].slice(:id).presence || params[:variable_params].slice(:key)
+ container.variables.find_by!(identifier) # rubocop:disable CodeReuse/ActiveRecord
+ end
+ end
+end
+
+::Ci::ChangeVariableService.prepend_if_ee('EE::Ci::ChangeVariableService')
diff --git a/app/services/ci/change_variables_service.rb b/app/services/ci/change_variables_service.rb
new file mode 100644
index 00000000000..3337eb09411
--- /dev/null
+++ b/app/services/ci/change_variables_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Ci
+ class ChangeVariablesService < BaseContainerService
+ def execute
+ container.update(params)
+ end
+ end
+end
+
+::Ci::ChangeVariablesService.prepend_if_ee('EE::Ci::ChangeVariablesService')
diff --git a/app/services/ci/create_cross_project_pipeline_service.rb b/app/services/ci/create_cross_project_pipeline_service.rb
index 1700312b941..23207d809d4 100644
--- a/app/services/ci/create_cross_project_pipeline_service.rb
+++ b/app/services/ci/create_cross_project_pipeline_service.rb
@@ -98,7 +98,7 @@ module Ci
end
def can_update_branch?(target_ref)
- ::Gitlab::UserAccess.new(current_user, project: downstream_project).can_update_branch?(target_ref)
+ ::Gitlab::UserAccess.new(current_user, container: downstream_project).can_update_branch?(target_ref)
end
def downstream_project
diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb
index 9a6e103e5dd..cd3807e0495 100644
--- a/app/services/ci/create_job_artifacts_service.rb
+++ b/app/services/ci/create_job_artifacts_service.rb
@@ -25,7 +25,7 @@ module Ci
if lsif?(artifact_type)
headers[:ProcessLsif] = true
- headers[:ProcessLsifReferences] = Feature.enabled?(:code_navigation_references, project, default_enabled: false)
+ headers[:ProcessLsifReferences] = Feature.enabled?(:code_navigation_references, project, default_enabled: true)
end
success(headers: headers)
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 2d7f5014aa9..70ad18e80eb 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -19,9 +19,13 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Validate::External,
Gitlab::Ci::Pipeline::Chain::Populate,
+ Gitlab::Ci::Pipeline::Chain::StopDryRun,
Gitlab::Ci::Pipeline::Chain::Create,
Gitlab::Ci::Pipeline::Chain::Limit::Activity,
- Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
+ Gitlab::Ci::Pipeline::Chain::Limit::JobActivity,
+ Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines,
+ Gitlab::Ci::Pipeline::Chain::Metrics,
+ Gitlab::Ci::Pipeline::Chain::Pipeline::Process].freeze
# Create a new pipeline in the specified project.
#
@@ -68,21 +72,14 @@ module Ci
bridge: bridge,
**extra_options(options))
- sequence = Gitlab::Ci::Pipeline::Chain::Sequence
- .new(pipeline, command, SEQUENCE)
+ # Ensure we never persist the pipeline when dry_run: true
+ @pipeline.readonly! if command.dry_run?
- sequence.build! do |pipeline, sequence|
- schedule_head_pipeline_update
+ Gitlab::Ci::Pipeline::Chain::Sequence
+ .new(pipeline, command, SEQUENCE)
+ .build!
- if sequence.complete?
- cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
- pipeline_created_counter.increment(source: source)
-
- Ci::ProcessPipelineService
- .new(pipeline)
- .execute(nil, initial_process: true)
- end
- end
+ schedule_head_pipeline_update if pipeline.persisted?
# If pipeline is not persisted, try to recover IID
pipeline.reset_project_iid unless pipeline.persisted? ||
@@ -110,38 +107,14 @@ module Ci
commit.try(:id)
end
- def cancel_pending_pipelines
- Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
- cancelables.find_each do |cancelable|
- cancelable.auto_cancel_running(pipeline)
- end
- end
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def auto_cancelable_pipelines
- project.ci_pipelines
- .where(ref: pipeline.ref)
- .where.not(id: pipeline.same_family_pipeline_ids)
- .where.not(sha: project.commit(pipeline.ref).try(:id))
- .alive_or_scheduled
- .with_only_interruptible_builds
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def pipeline_created_counter
- @pipeline_created_counter ||= Gitlab::Metrics
- .counter(:pipelines_created_total, "Counter of pipelines created")
- end
-
def schedule_head_pipeline_update
pipeline.all_merge_requests.opened.each do |merge_request|
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
end
- def extra_options(content: nil)
- { content: content }
+ def extra_options(content: nil, dry_run: false)
+ { content: content, dry_run: dry_run }
end
end
end
diff --git a/app/services/ci/create_web_ide_terminal_service.rb b/app/services/ci/create_web_ide_terminal_service.rb
index 29d40756ab4..4f1bf0447d2 100644
--- a/app/services/ci/create_web_ide_terminal_service.rb
+++ b/app/services/ci/create_web_ide_terminal_service.rb
@@ -32,7 +32,7 @@ module Ci
Ci::ProcessPipelineService
.new(pipeline)
- .execute(nil, initial_process: true)
+ .execute
pipeline_created_counter.increment(source: :webide)
end
diff --git a/app/services/ci/pipeline_processing/legacy_processing_service.rb b/app/services/ci/pipeline_processing/legacy_processing_service.rb
deleted file mode 100644
index 56fbc7271da..00000000000
--- a/app/services/ci/pipeline_processing/legacy_processing_service.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- module PipelineProcessing
- class LegacyProcessingService
- include Gitlab::Utils::StrongMemoize
-
- attr_reader :pipeline
-
- def initialize(pipeline)
- @pipeline = pipeline
- end
-
- def execute(trigger_build_ids = nil, initial_process: false)
- success = process_stages_for_stage_scheduling
-
- # we evaluate dependent needs,
- # only when the another job has finished
- success = process_dag_builds_without_needs || success if initial_process
- success = process_dag_builds_with_needs(trigger_build_ids) || success
-
- @pipeline.update_legacy_status
-
- success
- end
-
- private
-
- def process_stages_for_stage_scheduling
- stage_indexes_of_created_stage_scheduled_processables.flat_map do |index|
- process_stage_for_stage_scheduling(index)
- end.any?
- end
-
- def process_stage_for_stage_scheduling(index)
- current_status = status_for_prior_stages(index)
-
- return unless Ci::HasStatus::COMPLETED_STATUSES.include?(current_status)
-
- created_stage_scheduled_processables_in_stage(index).find_each.select do |build|
- process_build(build, current_status)
- end.any?
- end
-
- def process_dag_builds_without_needs
- created_processables.scheduling_type_dag.without_needs.each do |build|
- process_build(build, 'success')
- end
- end
-
- def process_dag_builds_with_needs(trigger_build_ids)
- return false unless trigger_build_ids.present?
-
- # we find processables that are dependent:
- # 1. because of current dependency,
- trigger_build_names = pipeline.processables.latest
- .for_ids(trigger_build_ids).names
-
- # 2. does not have builds that not yet complete
- incomplete_build_names = pipeline.processables.latest
- .incomplete.names
-
- # Each found processable is guaranteed here to have completed status
- created_processables
- .scheduling_type_dag
- .with_needs(trigger_build_names)
- .without_needs(incomplete_build_names)
- .find_each
- .map(&method(:process_dag_build_with_needs))
- .any?
- end
-
- def process_dag_build_with_needs(build)
- current_status = status_for_build_needs(build.needs.map(&:name))
-
- return unless Ci::HasStatus::COMPLETED_STATUSES.include?(current_status)
-
- process_build(build, current_status)
- end
-
- def process_build(build, current_status)
- Gitlab::OptimisticLocking.retry_lock(build) do |subject|
- Ci::ProcessBuildService.new(project, subject.user)
- .execute(subject, current_status)
- end
- end
-
- def status_for_prior_stages(index)
- pipeline.processables.status_for_prior_stages(index, project: pipeline.project)
- end
-
- def status_for_build_needs(needs)
- pipeline.processables.status_for_names(needs, project: pipeline.project)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def stage_indexes_of_created_stage_scheduled_processables
- created_stage_scheduled_processables.order(:stage_idx)
- .pluck(Arel.sql('DISTINCT stage_idx'))
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def created_stage_scheduled_processables_in_stage(index)
- created_stage_scheduled_processables
- .with_preloads
- .for_stage(index)
- end
-
- def created_stage_scheduled_processables
- created_processables.scheduling_type_stage
- end
-
- def created_processables
- pipeline.processables.created
- end
-
- def project
- pipeline.project
- end
- end
- end
-end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 1f24dce0458..d84ef5fbb93 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -8,20 +8,14 @@ module Ci
@pipeline = pipeline
end
- def execute(trigger_build_ids = nil, initial_process: false)
+ def execute
increment_processing_counter
update_retried
- if ::Gitlab::Ci::Features.atomic_processing?(pipeline.project)
- Ci::PipelineProcessing::AtomicProcessingService
- .new(pipeline)
- .execute
- else
- Ci::PipelineProcessing::LegacyProcessingService
- .new(pipeline)
- .execute(trigger_build_ids, initial_process: initial_process)
- end
+ Ci::PipelineProcessing::AtomicProcessingService
+ .new(pipeline)
+ .execute
end
def metrics
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 3797ea1d96c..04d620d1d38 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -107,23 +107,15 @@ module Ci
build.runner_id = runner.id
build.runner_session_attributes = params[:session] if params[:session].present?
- unless build.has_valid_build_dependencies?
- build.drop!(:missing_dependency_failure)
- return false
- end
-
- unless build.supported_runner?(params.dig(:info, :features))
- build.drop!(:runner_unsupported)
- return false
- end
+ failure_reason, _ = pre_assign_runner_checks.find { |_, check| check.call(build, params) }
- if build.archived?
- build.drop!(:archived_failure)
- return false
+ if failure_reason
+ build.drop!(failure_reason)
+ else
+ build.run!
end
- build.run!
- true
+ !failure_reason
end
def scheduler_failure!(build)
@@ -238,6 +230,14 @@ module Ci
def job_queue_duration_seconds
@job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS)
end
+
+ def pre_assign_runner_checks
+ {
+ missing_dependency_failure: -> (build, _) { !build.has_valid_build_dependencies? },
+ runner_unsupported: -> (build, params) { !build.supported_runner?(params.dig(:info, :features)) },
+ archived_failure: -> (build, _) { build.archived? }
+ }
+ end
end
end
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 4229be6c7d7..2f52f0a39c1 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -22,12 +22,6 @@ module Ci
needs += build.needs.map(&:name)
end
- # In a DAG, the dependencies may have already completed. Figure out
- # which builds have succeeded and use them to update the pipeline. If we don't
- # do this, then builds will be stuck in the created state since their dependencies
- # will never run.
- completed_build_ids = pipeline.find_successful_build_ids_by_names(needs) if needs.any?
-
pipeline.builds.latest.skipped.find_each do |skipped|
retry_optimistic_lock(skipped) { |build| build.process }
end
@@ -38,7 +32,7 @@ module Ci
Ci::ProcessPipelineService
.new(pipeline)
- .execute(completed_build_ids, initial_process: true)
+ .execute
end
end
end
diff --git a/app/services/clusters/aws/authorize_role_service.rb b/app/services/clusters/aws/authorize_role_service.rb
index 6eafce0597e..fb620f77b9f 100644
--- a/app/services/clusters/aws/authorize_role_service.rb
+++ b/app/services/clusters/aws/authorize_role_service.rb
@@ -23,7 +23,9 @@ module Clusters
@role = create_or_update_role!
Response.new(:ok, credentials)
- rescue *ERRORS
+ rescue *ERRORS => e
+ Gitlab::ErrorTracking.track_exception(e)
+
Response.new(:unprocessable_entity, {})
end
diff --git a/app/services/clusters/parse_cluster_applications_artifact_service.rb b/app/services/clusters/parse_cluster_applications_artifact_service.rb
index 6a0ca0ef9d0..b9b2953b6bd 100644
--- a/app/services/clusters/parse_cluster_applications_artifact_service.rb
+++ b/app/services/clusters/parse_cluster_applications_artifact_service.rb
@@ -5,7 +5,7 @@ module Clusters
include Gitlab::Utils::StrongMemoize
MAX_ACCEPTABLE_ARTIFACT_SIZE = 5.kilobytes
- RELEASE_NAMES = %w[prometheus cilium].freeze
+ RELEASE_NAMES = %w[cilium].freeze
def initialize(job, current_user)
@job = job
@@ -14,8 +14,6 @@ module Clusters
end
def execute(artifact)
- return success unless Feature.enabled?(:cluster_applications_artifact, project)
-
raise ArgumentError, 'Artifact is not cluster_applications file type' unless artifact&.cluster_applications?
return error(too_big_error_message, :bad_request) unless artifact.file.size < MAX_ACCEPTABLE_ARTIFACT_SIZE
@@ -46,6 +44,8 @@ module Clusters
releases = []
artifact.each_blob do |blob|
+ next if blob.empty?
+
releases.concat(Gitlab::Kubernetes::Helm::Parsers::ListV2.new(blob).releases)
end
diff --git a/app/services/cohorts_service.rb b/app/services/cohorts_service.rb
index 03be87f4cc1..7bc3b267a12 100644
--- a/app/services/cohorts_service.rb
+++ b/app/services/cohorts_service.rb
@@ -63,7 +63,7 @@ class CohortsService
overall_total = month_totals.first
month_totals.map do |total|
- { total: total, percentage: total.zero? ? 0 : 100 * total / overall_total }
+ { total: total, percentage: total == 0 ? 0 : 100 * total / overall_total }
end
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 661e654406e..edb9f04ccd7 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -22,7 +22,9 @@ module Commits
@branch_name,
message,
start_project: @start_project,
- start_branch_name: @start_branch)
+ start_branch_name: @start_branch,
+ dry_run: @dry_run
+ )
rescue Gitlab::Git::Repository::CreateTreeError => ex
act = action.to_s.dasherize
type = @commit.change_type_title(current_user)
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index d80d9bebe9c..a1498da302e 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -21,6 +21,7 @@ module Commits
@start_sha = params[:start_sha]
@branch_name = params[:branch_name]
@force = params[:force] || false
+ @dry_run = params[:dry_run] || false
end
def execute
@@ -69,7 +70,7 @@ module Commits
end
def validate_permissions!
- allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@branch_name)
+ allowed = ::Gitlab::UserAccess.new(current_user, container: project).can_push_to_branch?(@branch_name)
unless allowed
raise_error("You are not allowed to push into this branch")
diff --git a/app/services/concerns/incident_management/settings.rb b/app/services/concerns/incident_management/settings.rb
index 491bd4fa6bf..13a047ec106 100644
--- a/app/services/concerns/incident_management/settings.rb
+++ b/app/services/concerns/incident_management/settings.rb
@@ -1,8 +1,11 @@
# frozen_string_literal: true
+
module IncidentManagement
module Settings
include Gitlab::Utils::StrongMemoize
+ delegate :send_email?, to: :incident_management_setting
+
def incident_management_setting
strong_memoize(:incident_management_setting) do
project.incident_management_setting ||
diff --git a/app/services/design_management/move_designs_service.rb b/app/services/design_management/move_designs_service.rb
new file mode 100644
index 00000000000..de763caba2f
--- /dev/null
+++ b/app/services/design_management/move_designs_service.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class MoveDesignsService < DesignService
+ # @param user [User] The current user
+ # @param [Hash] params
+ # @option params [DesignManagement::Design] :current_design
+ # @option params [DesignManagement::Design] :previous_design (nil)
+ # @option params [DesignManagement::Design] :next_design (nil)
+ def initialize(user, params)
+ super(nil, user, params.merge(issue: nil))
+ end
+
+ def execute
+ return error(:no_focus) unless current_design.present?
+ return error(:cannot_move) unless ::Feature.enabled?(:reorder_designs, project, default_enabled: true)
+ return error(:cannot_move) unless current_user.can?(:move_design, current_design)
+ return error(:no_neighbors) unless neighbors.present?
+ return error(:not_distinct) unless all_distinct?
+ return error(:not_adjacent) if any_in_gap?
+ return error(:not_same_issue) unless all_same_issue?
+
+ move_nulls_to_end
+ current_design.move_between(previous_design, next_design)
+ current_design.save!
+ success
+ end
+
+ def error(message)
+ ServiceResponse.error(message: message)
+ end
+
+ def success
+ ServiceResponse.success
+ end
+
+ private
+
+ delegate :issue, :project, to: :current_design
+
+ def move_nulls_to_end
+ moved_records = current_design.class.move_nulls_to_end(issue.designs.in_creation_order)
+ return if moved_records == 0
+
+ current_design.reset
+ next_design&.reset
+ previous_design&.reset
+ end
+
+ def neighbors
+ [previous_design, next_design].compact
+ end
+
+ def all_distinct?
+ ids.uniq.size == ids.size
+ end
+
+ def any_in_gap?
+ return false unless previous_design&.relative_position && next_design&.relative_position
+
+ !previous_design.immediately_before?(next_design)
+ end
+
+ def all_same_issue?
+ issue.designs.id_in(ids).count == ids.size
+ end
+
+ def ids
+ @ids ||= [current_design, *neighbors].map(&:id)
+ end
+
+ def current_design
+ params[:current_design]
+ end
+
+ def previous_design
+ params[:previous_design]
+ end
+
+ def next_design
+ params[:next_design]
+ end
+ end
+end
diff --git a/app/services/discussions/capture_diff_note_position_service.rb b/app/services/discussions/capture_diff_note_position_service.rb
index 8f12470d9e8..4e8fd90a2e7 100644
--- a/app/services/discussions/capture_diff_note_position_service.rb
+++ b/app/services/discussions/capture_diff_note_position_service.rb
@@ -50,9 +50,9 @@ module Discussions
merge_ref_head = merge_request.merge_ref_head
return unless merge_ref_head
- start_sha, base_sha = merge_ref_head.parent_ids
+ start_sha, _ = merge_ref_head.parent_ids
new_diff_refs = Gitlab::Diff::DiffRefs.new(
- base_sha: base_sha,
+ base_sha: start_sha,
start_sha: start_sha,
head_sha: merge_ref_head.id)
diff --git a/app/services/discussions/resolve_service.rb b/app/services/discussions/resolve_service.rb
index 946fb5f1372..cd5925cd9be 100644
--- a/app/services/discussions/resolve_service.rb
+++ b/app/services/discussions/resolve_service.rb
@@ -56,7 +56,7 @@ module Discussions
def process_auto_merge
return unless merge_request
- return unless @resolved_count.positive?
+ return unless @resolved_count > 0
return unless discussions_ready_to_merge?
AutoMergeProcessWorker.perform_async(merge_request.id)
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index ad36fe70b3a..3921dbefd06 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -100,25 +100,21 @@ class EventCreateService
# @param [WikiPage::Meta] wiki_page_meta The event target
# @param [User] author The event author
# @param [Symbol] action One of the Event::WIKI_ACTIONS
+ # @param [String] fingerprint The de-duplication fingerprint
#
- # @return a tuple of event and either :found or :created
- def wiki_event(wiki_page_meta, author, action)
+ # The fingerprint, if provided, should be sufficient to find duplicate events.
+ # Suitable values would be, for example, the current page SHA.
+ #
+ # @return [Event] the event
+ def wiki_event(wiki_page_meta, author, action, fingerprint)
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
- if duplicate = existing_wiki_event(wiki_page_meta, action)
- return duplicate
- end
+ Gitlab::UsageDataCounters::TrackUniqueActions.track_event(event_action: action, event_target: wiki_page_meta.class, author_id: author.id)
- event = create_record_event(wiki_page_meta, author, action)
- # Ensure that the event is linked in time to the metadata, for non-deletes
- unless event.destroyed_action?
- time_stamp = wiki_page_meta.updated_at
- event.update_columns(updated_at: time_stamp, created_at: time_stamp)
- end
+ duplicate = Event.for_wiki_meta(wiki_page_meta).for_fingerprint(fingerprint).first
+ return duplicate if duplicate.present?
- Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: action, event_target: wiki_page_meta.class, author_id: author.id)
-
- event
+ create_record_event(wiki_page_meta, author, action, fingerprint.presence)
end
def approve_mr(merge_request, current_user)
@@ -127,45 +123,38 @@ class EventCreateService
private
- def existing_wiki_event(wiki_page_meta, action)
- if Event.actions.fetch(action) == Event.actions[:destroyed]
- most_recent = Event.for_wiki_meta(wiki_page_meta).recent.first
- return most_recent if most_recent.present? && Event.actions[most_recent.action] == Event.actions[action]
- else
- Event.for_wiki_meta(wiki_page_meta).created_at(wiki_page_meta.updated_at).first
- end
- end
-
- def create_record_event(record, current_user, status)
+ def create_record_event(record, current_user, status, fingerprint = nil)
create_event(record.resource_parent, current_user, status,
- target_id: record.id, target_type: record.class.name)
+ fingerprint: fingerprint,
+ target_id: record.id,
+ target_type: record.class.name)
end
# If creating several events, this method will insert them all in a single
# statement
#
- # @param [[Eventable, Symbol]] a list of pairs of records and a valid status
+ # @param [[Eventable, Symbol, String]] a list of tuples of records, a valid status, and fingerprint
# @param [User] the author of the event
- def create_record_events(pairs, current_user)
+ def create_record_events(tuples, current_user)
base_attrs = {
created_at: Time.now.utc,
updated_at: Time.now.utc,
author_id: current_user.id
}
- attribute_sets = pairs.map do |record, status|
+ attribute_sets = tuples.map do |record, status, fingerprint|
action = Event.actions[status]
raise IllegalActionError, "#{status} is not a valid status" if action.nil?
parent_attrs(record.resource_parent)
.merge(base_attrs)
- .merge(action: action, target_id: record.id, target_type: record.class.name)
+ .merge(action: action, fingerprint: fingerprint, target_id: record.id, target_type: record.class.name)
end
result = Event.insert_all(attribute_sets, returning: %w[id])
- pairs.each do |record, status|
- Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: status, event_target: record.class, author_id: current_user.id)
+ tuples.each do |record, status, _|
+ Gitlab::UsageDataCounters::TrackUniqueActions.track_event(event_action: status, event_target: record.class, author_id: current_user.id)
end
result
@@ -183,7 +172,7 @@ class EventCreateService
new_event
end
- Gitlab::UsageDataCounters::TrackUniqueActions.track_action(event_action: :pushed, event_target: Project, author_id: current_user.id)
+ Gitlab::UsageDataCounters::TrackUniqueActions.track_event(event_action: :pushed, event_target: Project, author_id: current_user.id)
Users::LastPushEventService.new(current_user)
.cache_last_push_event(event)
@@ -198,7 +187,11 @@ class EventCreateService
)
attributes.merge!(parent_attrs(resource_parent))
- Event.create!(attributes)
+ if attributes[:fingerprint].present?
+ Event.safe_find_or_create_by!(attributes)
+ else
+ Event.create!(attributes)
+ end
end
def parent_attrs(resource_parent)
diff --git a/app/services/git/process_ref_changes_service.rb b/app/services/git/process_ref_changes_service.rb
index 6d1ff97016b..c012c61a337 100644
--- a/app/services/git/process_ref_changes_service.rb
+++ b/app/services/git/process_ref_changes_service.rb
@@ -75,8 +75,6 @@ module Git
end
def merge_request_branches_for(changes)
- return if Feature.disabled?(:refresh_only_existing_merge_requests_on_push, default_enabled: true)
-
@merge_requests_branches ||= MergeRequests::PushedBranchesService.new(project, current_user, changes: changes).execute
end
end
diff --git a/app/services/git/tag_push_service.rb b/app/services/git/tag_push_service.rb
index 120c4cde94b..641fe8e3916 100644
--- a/app/services/git/tag_push_service.rb
+++ b/app/services/git/tag_push_service.rb
@@ -26,9 +26,5 @@ module Git
def removing_tag?
Gitlab::Git.blank_ref?(newrev)
end
-
- def tag_name
- Gitlab::Git.ref_name(ref)
- end
end
end
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index b3937a10a70..f9de72f2d5f 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -41,7 +41,12 @@ module Git
end
def create_event_for(change)
- event_service.execute(change.last_known_slug, change.page, change.event_action)
+ event_service.execute(
+ change.last_known_slug,
+ change.page,
+ change.event_action,
+ change.sha
+ )
end
def event_service
diff --git a/app/services/git/wiki_push_service/change.rb b/app/services/git/wiki_push_service/change.rb
index 14e622dd147..562c43487e9 100644
--- a/app/services/git/wiki_push_service/change.rb
+++ b/app/services/git/wiki_push_service/change.rb
@@ -33,6 +33,10 @@ module Git
strip_extension(raw_change.old_path || raw_change.new_path)
end
+ def sha
+ change[:newrev]
+ end
+
private
attr_reader :raw_change, :change, :wiki
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index f2fb494500d..2bd571f60af 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -47,6 +47,19 @@ module Groups
raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path?
raise_transfer_error(:group_contains_images) if group_projects_contain_registry_images?
raise_transfer_error(:cannot_transfer_to_subgroup) if transfer_to_subgroup?
+ raise_transfer_error(:group_contains_npm_packages) if group_with_npm_packages?
+ end
+
+ def group_with_npm_packages?
+ return false unless group.packages_feature_enabled?
+
+ npm_packages = ::Packages::GroupPackagesFinder.new(current_user, group, package_type: :npm).execute
+
+ different_root_ancestor? && npm_packages.exists?
+ end
+
+ def different_root_ancestor?
+ group.root_ancestor != new_parent_group&.root_ancestor
end
def group_is_already_root?
@@ -144,7 +157,8 @@ module Groups
same_parent_as_current: s_('TransferGroup|Group is already associated to the parent group.'),
invalid_policies: s_("TransferGroup|You don't have enough permissions."),
group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.'),
- cannot_transfer_to_subgroup: s_('TransferGroup|Cannot transfer group to one of its subgroup.')
+ cannot_transfer_to_subgroup: s_('TransferGroup|Cannot transfer group to one of its subgroup.'),
+ group_contains_npm_packages: s_('TransferGroup|Group contains projects with NPM packages.')
}.freeze
end
end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 948540619ae..81393681dc0 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -17,6 +17,8 @@ module Groups
return false unless valid_share_with_group_lock_change?
+ return false unless valid_path_change_with_npm_packages?
+
before_assignment_hook(group, params)
group.assign_attributes(params)
@@ -36,6 +38,20 @@ module Groups
private
+ def valid_path_change_with_npm_packages?
+ return true unless group.packages_feature_enabled?
+ return true if params[:path].blank?
+ return true if !group.has_parent? && group.path == params[:path]
+
+ npm_packages = ::Packages::GroupPackagesFinder.new(current_user, group, package_type: :npm).execute
+ if npm_packages.exists?
+ group.errors.add(:path, s_('GroupSettings|cannot change when group contains projects with NPM packages'))
+ return
+ end
+
+ true
+ end
+
def before_assignment_hook(group, params)
# overridden in EE
end
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
index 0cf17568c78..a2923b1e4f9 100644
--- a/app/services/import/github_service.rb
+++ b/app/services/import/github_service.rb
@@ -33,7 +33,7 @@ module Import
end
def repo
- @repo ||= client.repo(params[:repo_id].to_i)
+ @repo ||= client.repository(params[:repo_id].to_i)
end
def project_name
diff --git a/app/services/incident_management/create_incident_label_service.rb b/app/services/incident_management/create_incident_label_service.rb
index dbd0d78fa3c..595f5df184f 100644
--- a/app/services/incident_management/create_incident_label_service.rb
+++ b/app/services/incident_management/create_incident_label_service.rb
@@ -14,27 +14,9 @@ module IncidentManagement
def execute
label = Labels::FindOrCreateService
.new(current_user, project, **LABEL_PROPERTIES)
- .execute
-
- if label.invalid?
- log_invalid_label_info(label)
- return ServiceResponse.error(payload: { label: label }, message: full_error_message(label))
- end
+ .execute(skip_authorization: true)
ServiceResponse.success(payload: { label: label })
end
-
- private
-
- def log_invalid_label_info(label)
- log_info <<~TEXT.chomp
- Cannot create incident label "#{label.title}" \
- for "#{label.project.full_name}": #{full_error_message(label)}.
- TEXT
- end
-
- def full_error_message(label)
- label.errors.full_messages.to_sentence
- end
end
end
diff --git a/app/services/incident_management/create_issue_service.rb b/app/services/incident_management/create_issue_service.rb
deleted file mode 100644
index 5e1e0863115..00000000000
--- a/app/services/incident_management/create_issue_service.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-# frozen_string_literal: true
-
-module IncidentManagement
- class CreateIssueService < BaseService
- include Gitlab::Utils::StrongMemoize
-
- def initialize(project, params)
- super(project, User.alert_bot, params)
- end
-
- def execute
- return error_with('setting disabled') unless incident_management_setting.create_issue?
- return error_with('invalid alert') unless alert.valid?
-
- issue = create_issue
- return error_with(issue_errors(issue)) unless issue.valid?
-
- success(issue: issue)
- end
-
- private
-
- def create_issue
- label_result = find_or_create_incident_label
-
- # Create an unlabelled issue if we couldn't create the label
- # due to a race condition.
- # See https://gitlab.com/gitlab-org/gitlab-foss/issues/65042
- extra_params = label_result.success? ? { label_ids: [label_result.payload[:label].id] } : {}
-
- Issues::CreateService.new(
- project,
- current_user,
- title: issue_title,
- description: issue_description,
- **extra_params
- ).execute
- end
-
- def issue_title
- alert.full_title
- end
-
- def issue_description
- horizontal_line = "\n\n---\n\n"
-
- [
- alert_summary,
- alert_markdown,
- issue_template_content
- ].compact.join(horizontal_line)
- end
-
- def find_or_create_incident_label
- IncidentManagement::CreateIncidentLabelService.new(project, current_user).execute
- end
-
- def alert_summary
- alert.issue_summary_markdown
- end
-
- def alert_markdown
- alert.alert_markdown
- end
-
- def alert
- strong_memoize(:alert) do
- Gitlab::Alerting::Alert.new(project: project, payload: params).present
- end
- end
-
- def issue_template_content
- incident_management_setting.issue_template_content
- end
-
- def incident_management_setting
- strong_memoize(:incident_management_setting) do
- project.incident_management_setting ||
- project.build_incident_management_setting
- end
- end
-
- def issue_errors(issue)
- issue.errors.full_messages.to_sentence
- end
-
- def error_with(message)
- log_error(%{Cannot create incident issue for "#{project.full_name}": #{message}})
-
- error(message)
- end
- end
-end
diff --git a/app/services/incident_management/incidents/create_service.rb b/app/services/incident_management/incidents/create_service.rb
new file mode 100644
index 00000000000..7206eaf51b2
--- /dev/null
+++ b/app/services/incident_management/incidents/create_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ module Incidents
+ class CreateService < BaseService
+ ISSUE_TYPE = 'incident'
+
+ def initialize(project, current_user, title:, description:)
+ super(project, current_user)
+
+ @title = title
+ @description = description
+ end
+
+ def execute
+ issue = Issues::CreateService.new(
+ project,
+ current_user,
+ title: title,
+ description: description,
+ label_ids: [find_or_create_incident_label.id],
+ issue_type: ISSUE_TYPE
+ ).execute
+
+ return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?
+
+ success(issue)
+ end
+
+ private
+
+ attr_reader :title, :description
+
+ def find_or_create_incident_label
+ IncidentManagement::CreateIncidentLabelService
+ .new(project, current_user)
+ .execute
+ .payload[:label]
+ end
+
+ def success(issue)
+ ServiceResponse.success(payload: { issue: issue })
+ end
+
+ def error(message, issue = nil)
+ ServiceResponse.error(payload: { issue: issue }, message: message)
+ end
+ end
+ end
+end
diff --git a/app/services/incident_management/pager_duty/create_incident_issue_service.rb b/app/services/incident_management/pager_duty/create_incident_issue_service.rb
index ee0feb49e0d..0c9ca2c0add 100644
--- a/app/services/incident_management/pager_duty/create_incident_issue_service.rb
+++ b/app/services/incident_management/pager_duty/create_incident_issue_service.rb
@@ -12,46 +12,30 @@ module IncidentManagement
def execute
return forbidden unless webhook_available?
- issue = create_issue
- return error(issue.errors.full_messages.to_sentence, issue) unless issue.valid?
-
- success(issue)
+ create_incident
end
private
alias_method :incident_payload, :params
- def create_issue
- label_result = find_or_create_incident_label
-
- # Create an unlabelled issue if we couldn't create the label
- # due to a race condition.
- # See https://gitlab.com/gitlab-org/gitlab-foss/issues/65042
- extra_params = label_result.success? ? { label_ids: [label_result.payload[:label].id] } : {}
-
- Issues::CreateService.new(
+ def create_incident
+ ::IncidentManagement::Incidents::CreateService.new(
project,
current_user,
title: issue_title,
- description: issue_description,
- **extra_params
+ description: issue_description
).execute
end
def webhook_available?
- Feature.enabled?(:pagerduty_webhook, project) &&
- incident_management_setting.pagerduty_active?
+ incident_management_setting.pagerduty_active?
end
def forbidden
ServiceResponse.error(message: 'Forbidden', http_status: :forbidden)
end
- def find_or_create_incident_label
- ::IncidentManagement::CreateIncidentLabelService.new(project, current_user).execute
- end
-
def issue_title
incident_payload['title']
end
@@ -59,14 +43,6 @@ module IncidentManagement
def issue_description
Gitlab::IncidentManagement::PagerDuty::IncidentIssueDescription.new(incident_payload).to_s
end
-
- def success(issue)
- ServiceResponse.success(payload: { issue: issue })
- end
-
- def error(message, issue = nil)
- ServiceResponse.error(payload: { issue: issue }, message: message)
- end
end
end
end
diff --git a/app/services/incident_management/pager_duty/process_webhook_service.rb b/app/services/incident_management/pager_duty/process_webhook_service.rb
index 5dd3186694a..fd8252f75fb 100644
--- a/app/services/incident_management/pager_duty/process_webhook_service.rb
+++ b/app/services/incident_management/pager_duty/process_webhook_service.rb
@@ -39,8 +39,7 @@ module IncidentManagement
end
def webhook_setting_active?
- Feature.enabled?(:pagerduty_webhook, project) &&
- incident_management_setting.pagerduty_active?
+ incident_management_setting.pagerduty_active?
end
def valid_token?(token)
diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb
index 0d1640924e5..b2f9c083b5b 100644
--- a/app/services/issuable/clone/base_service.rb
+++ b/app/services/issuable/clone/base_service.rb
@@ -24,12 +24,34 @@ module Issuable
private
+ def copy_award_emoji
+ AwardEmojis::CopyService.new(original_entity, new_entity).execute
+ end
+
+ def copy_notes
+ Notes::CopyService.new(current_user, original_entity, new_entity).execute
+ end
+
def update_new_entity
- rewriters = [ContentRewriter, AttributesRewriter]
+ update_new_entity_description
+ update_new_entity_attributes
+ copy_award_emoji
+ copy_notes
+ end
- rewriters.each do |rewriter|
- rewriter.new(current_user, original_entity, new_entity).execute
- end
+ def update_new_entity_description
+ rewritten_description = MarkdownContentRewriterService.new(
+ current_user,
+ original_entity.description,
+ original_entity.project,
+ new_parent
+ ).execute
+
+ new_entity.update!(description: rewritten_description)
+ end
+
+ def update_new_entity_attributes
+ AttributesRewriter.new(current_user, original_entity, new_entity).execute
end
def update_old_entity
@@ -47,7 +69,7 @@ module Issuable
end
def new_parent
- new_entity.project || new_entity.group
+ new_entity.resource_parent
end
def group
diff --git a/app/services/issuable/clone/content_rewriter.rb b/app/services/issuable/clone/content_rewriter.rb
deleted file mode 100644
index 67d2f9fd3fe..00000000000
--- a/app/services/issuable/clone/content_rewriter.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-module Issuable
- module Clone
- class ContentRewriter < ::Issuable::Clone::BaseService
- def initialize(current_user, original_entity, new_entity)
- @current_user = current_user
- @original_entity = original_entity
- @new_entity = new_entity
- @project = original_entity.project
- end
-
- def execute
- rewrite_description
- rewrite_award_emoji(original_entity, new_entity)
- rewrite_notes
- end
-
- private
-
- def rewrite_description
- new_entity.update(description: rewrite_content(original_entity.description))
- end
-
- def rewrite_notes
- new_discussion_ids = {}
- original_entity.notes_with_associations.find_each do |note|
- new_note = note.dup
- new_discussion_ids[note.discussion_id] ||= Discussion.discussion_id(new_note)
- new_params = {
- project: new_entity.project,
- noteable: new_entity,
- discussion_id: new_discussion_ids[note.discussion_id],
- note: rewrite_content(new_note.note),
- note_html: nil,
- created_at: note.created_at,
- updated_at: note.updated_at
- }
-
- if note.system_note_metadata
- new_params[:system_note_metadata] = note.system_note_metadata.dup
-
- # TODO: Implement copying of description versions when an issue is moved
- # https://gitlab.com/gitlab-org/gitlab/issues/32300
- new_params[:system_note_metadata].description_version = nil
- end
-
- new_note.update(new_params)
-
- rewrite_award_emoji(note, new_note)
- end
- end
-
- def rewrite_content(content)
- return unless content
-
- rewriters = [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter]
-
- rewriters.inject(content) do |text, klass|
- rewriter = klass.new(text, old_project, current_user)
- rewriter.rewrite(new_parent)
- end
- end
-
- def rewrite_award_emoji(old_awardable, new_awardable)
- old_awardable.award_emoji.each do |award|
- new_award = award.dup
- new_award.awardable = new_awardable
- new_award.save
- end
- end
- end
- end
-end
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index 195616857dc..84024cca68c 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -108,7 +108,7 @@ module Issuable
end
def milestone_changes_tracking_enabled?
- ::Feature.enabled?(:track_resource_milestone_change_events, issuable.project)
+ ::Feature.enabled?(:track_resource_milestone_change_events, issuable.project, default_enabled: true)
end
def create_due_date_note
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index e62315de5f9..2de6ed9fa1c 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -66,7 +66,7 @@ module Issues
def whitelisted_issue_params
base_params = [:title, :description, :confidential]
- admin_params = [:milestone_id]
+ admin_params = [:milestone_id, :issue_type]
if can?(current_user, :admin_issue, project)
params.slice(*(base_params + admin_params))
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 8594808cd44..e431c766df8 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -33,6 +33,7 @@ module Issues
notification_service.async.close_issue(issue, current_user, closed_via: closed_via) if notifications
todo_service.close_issue(issue, current_user)
+ resolve_alert(issue)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
@@ -58,6 +59,22 @@ module Issues
SystemNoteService.change_status(issue, issue.project, current_user, issue.state, current_commit)
end
+ def resolve_alert(issue)
+ return unless alert = issue.alert_management_alert
+ return if alert.resolved?
+
+ if alert.resolve
+ SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project, author: current_user).closed_alert_issue(issue)
+ else
+ Gitlab::AppLogger.warn(
+ message: 'Cannot resolve an associated Alert Management alert',
+ issue_id: issue.id,
+ alert_id: alert.id,
+ alert_errors: alert.errors.messages
+ )
+ end
+ end
+
def store_first_mentioned_in_commit_at(issue, merge_request)
metrics = issue.metrics
return if metrics.nil? || metrics.first_mentioned_in_commit_at
diff --git a/app/services/issues/reorder_service.rb b/app/services/issues/reorder_service.rb
index 02c18d31b5e..c82ad6ea501 100644
--- a/app/services/issues/reorder_service.rb
+++ b/app/services/issues/reorder_service.rb
@@ -40,7 +40,7 @@ module Issues
def move_between_ids
ids = [params[:move_after_id], params[:move_before_id]]
.map(&:to_i)
- .map { |m| m.positive? ? m : nil }
+ .map { |m| m > 0 ? m : nil }
ids.any? ? ids : nil
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 8d22f0edcdd..ac7baba3b7c 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -22,7 +22,7 @@ module Issues
end
def after_update(issue)
- IssuesChannel.broadcast_to(issue, event: 'updated') if Feature.enabled?(:broadcast_issue_updates, issue.project)
+ IssuesChannel.broadcast_to(issue, event: 'updated') if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project)
end
def handle_changes(issue, options)
@@ -43,7 +43,7 @@ module Issues
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.async.reassigned_issue(issue, current_user, old_assignees)
- todo_service.reassigned_issuable(issue, current_user, old_assignees)
+ todo_service.reassigned_assignable(issue, current_user, old_assignees)
end
if issue.previous_changes.include?('confidential')
diff --git a/app/services/jira/requests/projects/list_service.rb b/app/services/jira/requests/projects/list_service.rb
index 8ecfd358ffb..373c536974a 100644
--- a/app/services/jira/requests/projects/list_service.rb
+++ b/app/services/jira/requests/projects/list_service.rb
@@ -6,7 +6,7 @@ module Jira
class ListService < Base
extend ::Gitlab::Utils::Override
- def initialize(jira_service, params: {})
+ def initialize(jira_service, params = {})
super(jira_service, params)
@query = params[:query]
@@ -33,9 +33,9 @@ module Jira
end
def match_query?(jira_project)
- query = query.to_s.downcase
+ downcase_query = query.to_s.downcase
- jira_project&.key&.downcase&.include?(query) || jira_project&.name&.downcase&.include?(query)
+ jira_project&.key&.downcase&.include?(downcase_query) || jira_project&.name&.downcase&.include?(downcase_query)
end
def empty_payload
diff --git a/app/services/jira_import/cloud_users_mapper_service.rb b/app/services/jira_import/cloud_users_mapper_service.rb
new file mode 100644
index 00000000000..b1c7aac584f
--- /dev/null
+++ b/app/services/jira_import/cloud_users_mapper_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module JiraImport
+ class CloudUsersMapperService < UsersMapperService
+ private
+
+ def url
+ "/rest/api/2/users?maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}"
+ end
+
+ def jira_user_id(jira_user)
+ jira_user['accountId']
+ end
+
+ def jira_user_name(jira_user)
+ jira_user['displayName']
+ end
+ end
+end
diff --git a/app/services/jira_import/server_users_mapper_service.rb b/app/services/jira_import/server_users_mapper_service.rb
new file mode 100644
index 00000000000..d38d134f55c
--- /dev/null
+++ b/app/services/jira_import/server_users_mapper_service.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module JiraImport
+ class ServerUsersMapperService < UsersMapperService
+ private
+
+ def url
+ "/rest/api/2/user/search?username=''&maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}"
+ end
+
+ def jira_user_id(jira_user)
+ jira_user['key']
+ end
+
+ def jira_user_name(jira_user)
+ jira_user['name']
+ end
+ end
+end
diff --git a/app/services/jira_import/start_import_service.rb b/app/services/jira_import/start_import_service.rb
index f85f686c61a..88cfe684125 100644
--- a/app/services/jira_import/start_import_service.rb
+++ b/app/services/jira_import/start_import_service.rb
@@ -42,7 +42,7 @@ module JiraImport
ServiceResponse.success(payload: { import_data: jira_import } )
rescue => ex
- # in case project.save! raises an erorr
+ # in case project.save! raises an error
Gitlab::ErrorTracking.track_exception(ex, project_id: project.id)
jira_import&.do_fail!(error_message: ex.message)
build_error_response(ex.message)
diff --git a/app/services/jira_import/users_importer.rb b/app/services/jira_import/users_importer.rb
index 579d3675073..9babd468d56 100644
--- a/app/services/jira_import/users_importer.rb
+++ b/app/services/jira_import/users_importer.rb
@@ -2,9 +2,7 @@
module JiraImport
class UsersImporter
- attr_reader :user, :project, :start_at, :result
-
- MAX_USERS = 50
+ attr_reader :user, :project, :start_at
def initialize(user, project, start_at)
@project = project
@@ -15,29 +13,43 @@ module JiraImport
def execute
Gitlab::JiraImport.validate_project_settings!(project, user: user)
- return ServiceResponse.success(payload: nil) if users.blank?
-
- result = UsersMapper.new(project, users).execute
- ServiceResponse.success(payload: result)
+ ServiceResponse.success(payload: mapped_users)
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => error
- Gitlab::ErrorTracking.track_exception(error, project_id: project.id, request: url)
- ServiceResponse.error(message: "There was an error when communicating to Jira: #{error.message}")
+ Gitlab::ErrorTracking.track_exception(error, project_id: project.id)
+ ServiceResponse.error(message: "There was an error when communicating to Jira")
rescue Projects::ImportService::Error => error
ServiceResponse.error(message: error.message)
end
private
- def users
- @users ||= client.get(url)
+ def mapped_users
+ users_mapper_service.execute
+ end
+
+ def users_mapper_service
+ @users_mapper_service ||= user_mapper_service_factory
end
- def url
- "/rest/api/2/users?maxResults=#{MAX_USERS}&startAt=#{start_at.to_i}"
+ def deployment_type
+ # TODO: use project.jira_service.deployment_type value when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
+ @deployment_type ||= client.ServerInfo.all.deploymentType
end
def client
@client ||= project.jira_service.client
end
+
+ def user_mapper_service_factory
+ # TODO: use deployment_type enum from jira service when https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37003 is merged
+ case deployment_type.upcase
+ when JiraService::DEPLOYMENT_TYPES[:server]
+ ServerUsersMapperService.new(project.jira_service, start_at)
+ when JiraService::DEPLOYMENT_TYPES[:cloud]
+ CloudUsersMapperService.new(project.jira_service, start_at)
+ else
+ raise ArgumentError
+ end
+ end
end
end
diff --git a/app/services/jira_import/users_mapper.rb b/app/services/jira_import/users_mapper.rb
deleted file mode 100644
index c3cbeb157bd..00000000000
--- a/app/services/jira_import/users_mapper.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-module JiraImport
- class UsersMapper
- attr_reader :project, :jira_users
-
- def initialize(project, jira_users)
- @project = project
- @jira_users = jira_users
- end
-
- def execute
- jira_users.to_a.map do |jira_user|
- {
- jira_account_id: jira_user['accountId'],
- jira_display_name: jira_user['displayName'],
- jira_email: jira_user['emailAddress']
- }.merge(match_user(jira_user))
- end
- end
-
- private
-
- # TODO: Matching user by email and displayName will be done as the part
- # of follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/219023
- def match_user(jira_user)
- { gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }
- end
- end
-end
diff --git a/app/services/jira_import/users_mapper_service.rb b/app/services/jira_import/users_mapper_service.rb
new file mode 100644
index 00000000000..b5997d77215
--- /dev/null
+++ b/app/services/jira_import/users_mapper_service.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module JiraImport
+ class UsersMapperService
+ MAX_USERS = 50
+
+ attr_reader :jira_service, :start_at
+
+ def initialize(jira_service, start_at)
+ @jira_service = jira_service
+ @start_at = start_at
+ end
+
+ def execute
+ users.to_a.map do |jira_user|
+ {
+ jira_account_id: jira_user_id(jira_user),
+ jira_display_name: jira_user_name(jira_user),
+ jira_email: jira_user['emailAddress']
+ }.merge(match_user(jira_user))
+ end
+ end
+
+ private
+
+ def users
+ @users ||= client.get(url)
+ end
+
+ def client
+ @client ||= jira_service.client
+ end
+
+ def url
+ raise NotImplementedError
+ end
+
+ def jira_user_id(jira_user)
+ raise NotImplementedError
+ end
+
+ def jira_user_name(jira_user)
+ raise NotImplementedError
+ end
+
+ # TODO: Matching user by email and displayName will be done as the part
+ # of follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/219023
+ def match_user(jira_user)
+ { gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }
+ end
+ end
+end
diff --git a/app/services/labels/available_labels_service.rb b/app/services/labels/available_labels_service.rb
index 3b226f39d04..1d022740c44 100644
--- a/app/services/labels/available_labels_service.rb
+++ b/app/services/labels/available_labels_service.rb
@@ -30,7 +30,7 @@ module Labels
end
def filter_labels_ids_in_param(key)
- ids = params[key].to_a
+ ids = Array.wrap(params[key])
return [] if ids.empty?
# rubocop:disable CodeReuse/ActiveRecord
@@ -39,12 +39,12 @@ module Labels
ids.map(&:to_i) & existing_ids
end
- private
-
def available_labels
@available_labels ||= LabelsFinder.new(current_user, finder_params).execute
end
+ private
+
def finder_params
params = { include_ancestor_groups: true }
diff --git a/app/services/markdown_content_rewriter_service.rb b/app/services/markdown_content_rewriter_service.rb
new file mode 100644
index 00000000000..bc6fd592eaa
--- /dev/null
+++ b/app/services/markdown_content_rewriter_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# This service passes Markdown content through our GFM rewriter classes
+# which rewrite references to GitLab objects and uploads within the content
+# based on their visibility by the `target_parent`.
+class MarkdownContentRewriterService
+ REWRITERS = [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter].freeze
+
+ def initialize(current_user, content, source_parent, target_parent)
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39654#note_399095117
+ raise ArgumentError, 'The rewriter classes require that `source_parent` is a `Project`' \
+ unless source_parent.is_a?(Project)
+
+ @current_user = current_user
+ @content = content.presence
+ @source_parent = source_parent
+ @target_parent = target_parent
+ end
+
+ def execute
+ return unless content
+
+ REWRITERS.inject(content) do |text, klass|
+ rewriter = klass.new(text, source_parent, current_user)
+ rewriter.rewrite(target_parent)
+ end
+ end
+
+ private
+
+ attr_reader :current_user, :content, :source_parent, :target_parent
+end
diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb
index bc681397039..f0c85ae03c9 100644
--- a/app/services/merge_requests/after_create_service.rb
+++ b/app/services/merge_requests/after_create_service.rb
@@ -14,3 +14,5 @@ module MergeRequests
end
end
end
+
+MergeRequests::AfterCreateService.prepend_if_ee('EE::MergeRequests::AfterCreateService')
diff --git a/app/services/merge_requests/conflicts/list_service.rb b/app/services/merge_requests/conflicts/list_service.rb
index c6b3a6a1a69..30a493e91ce 100644
--- a/app/services/merge_requests/conflicts/list_service.rb
+++ b/app/services/merge_requests/conflicts/list_service.rb
@@ -8,7 +8,7 @@ module MergeRequests
def can_be_resolved_by?(user)
return false unless merge_request.source_project
- access = ::Gitlab::UserAccess.new(user, project: merge_request.source_project)
+ access = ::Gitlab::UserAccess.new(user, container: merge_request.source_project)
access.can_push_to_branch?(merge_request.source_branch)
end
diff --git a/app/services/merge_requests/ff_merge_service.rb b/app/services/merge_requests/ff_merge_service.rb
index b3896d61a78..79011094e88 100644
--- a/app/services/merge_requests/ff_merge_service.rb
+++ b/app/services/merge_requests/ff_merge_service.rb
@@ -22,6 +22,7 @@ module MergeRequests
ff_merge
rescue Gitlab::Git::PreReceiveError => e
+ Gitlab::ErrorTracking.track_exception(e, pre_receive_message: e.raw_message, merge_request_id: merge_request&.id)
raise MergeError, e.message
rescue StandardError => e
raise MergeError, "Something went wrong during merge: #{e.message}"
diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb
index d3d661a3b75..a3c39fa2e32 100644
--- a/app/services/merge_requests/mergeability_check_service.rb
+++ b/app/services/merge_requests/mergeability_check_service.rb
@@ -125,7 +125,7 @@ module MergeRequests
end
def update_diff_discussion_positions!
- return if Feature.disabled?(:merge_ref_head_comments, merge_request.target_project)
+ return if Feature.disabled?(:merge_ref_head_comments, merge_request.target_project, default_enabled: true)
Discussions::CaptureDiffNotePositionsService.new(merge_request).execute
end
diff --git a/app/services/merge_requests/pushed_branches_service.rb b/app/services/merge_requests/pushed_branches_service.rb
index afcf0f7678a..bbe75305d92 100644
--- a/app/services/merge_requests/pushed_branches_service.rb
+++ b/app/services/merge_requests/pushed_branches_service.rb
@@ -9,7 +9,7 @@ module MergeRequests
def execute
return [] if branch_names.blank?
- source_branches = project.source_of_merge_requests.opened
+ source_branches = project.source_of_merge_requests.open_and_closed
.from_source_branches(branch_names).pluck(:source_branch)
target_branches = project.merge_requests.opened
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 29e0c22b155..cf02158b629 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -105,7 +105,7 @@ module MergeRequests
def handle_assignees_change(merge_request, old_assignees)
create_assignee_note(merge_request, old_assignees)
notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
- todo_service.reassigned_issuable(merge_request, current_user, old_assignees)
+ todo_service.reassigned_assignable(merge_request, current_user, old_assignees)
end
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
diff --git a/app/services/metrics/dashboard/base_service.rb b/app/services/metrics/dashboard/base_service.rb
index 5fa127d64b2..5be8ae62548 100644
--- a/app/services/metrics/dashboard/base_service.rb
+++ b/app/services/metrics/dashboard/base_service.rb
@@ -13,7 +13,7 @@ module Metrics
STAGES::MetricEndpointInserter,
STAGES::VariableEndpointInserter,
STAGES::PanelIdsInserter,
- STAGES::Sorter,
+ STAGES::TrackPanelType,
STAGES::AlertsInserter,
STAGES::UrlValidator
].freeze
@@ -34,7 +34,7 @@ module Metrics
# Returns an un-processed dashboard from the cache.
def raw_dashboard
- Gitlab::Metrics::Dashboard::Cache.fetch(cache_key) { get_raw_dashboard }
+ Gitlab::Metrics::Dashboard::Cache.for(project).fetch(cache_key) { get_raw_dashboard }
end
# Should return true if this dashboard service is for an out-of-the-box
diff --git a/app/services/metrics/dashboard/clone_dashboard_service.rb b/app/services/metrics/dashboard/clone_dashboard_service.rb
index a6bece391f2..d9bd9423a1b 100644
--- a/app/services/metrics/dashboard/clone_dashboard_service.rb
+++ b/app/services/metrics/dashboard/clone_dashboard_service.rb
@@ -9,12 +9,11 @@ module Metrics
include Gitlab::Utils::StrongMemoize
ALLOWED_FILE_TYPE = '.yml'
- USER_DASHBOARDS_DIR = ::Metrics::Dashboard::CustomDashboardService::DASHBOARD_ROOT
+ USER_DASHBOARDS_DIR = ::Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT
SEQUENCES = {
::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH => [
::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter,
- ::Gitlab::Metrics::Dashboard::Stages::CustomMetricsInserter,
- ::Gitlab::Metrics::Dashboard::Stages::Sorter
+ ::Gitlab::Metrics::Dashboard::Stages::CustomMetricsInserter
].freeze,
::Metrics::Dashboard::SelfMonitoringDashboardService::DASHBOARD_PATH => [
@@ -22,8 +21,7 @@ module Metrics
].freeze,
::Metrics::Dashboard::ClusterDashboardService::DASHBOARD_PATH => [
- ::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter,
- ::Gitlab::Metrics::Dashboard::Stages::Sorter
+ ::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter
].freeze
}.freeze
@@ -112,7 +110,7 @@ module Metrics
end
def push_authorized?
- Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch)
+ Gitlab::UserAccess.new(current_user, container: project).can_push_to_branch?(branch)
end
def dashboard_template
diff --git a/app/services/metrics/dashboard/cluster_dashboard_service.rb b/app/services/metrics/dashboard/cluster_dashboard_service.rb
index bfd5abf1126..4a28e847fdd 100644
--- a/app/services/metrics/dashboard/cluster_dashboard_service.rb
+++ b/app/services/metrics/dashboard/cluster_dashboard_service.rb
@@ -9,12 +9,11 @@ module Metrics
DASHBOARD_NAME = 'Cluster'
# SHA256 hash of dashboard content
- DASHBOARD_VERSION = '9349afc1d96329c08ab478ea0b77db94ee5cc2549b8c754fba67a7f424666b22'
+ DASHBOARD_VERSION = 'e1a4f8cc2c044cf32273af2cd775eb484729baac0995db687d81d92686bf588e'
SEQUENCE = [
STAGES::ClusterEndpointInserter,
- STAGES::PanelIdsInserter,
- STAGES::Sorter
+ STAGES::PanelIdsInserter
].freeze
class << self
diff --git a/app/services/metrics/dashboard/custom_dashboard_service.rb b/app/services/metrics/dashboard/custom_dashboard_service.rb
index 741738cc3af..f0f19bf2ba3 100644
--- a/app/services/metrics/dashboard/custom_dashboard_service.rb
+++ b/app/services/metrics/dashboard/custom_dashboard_service.rb
@@ -6,16 +6,13 @@
module Metrics
module Dashboard
class CustomDashboardService < ::Metrics::Dashboard::BaseService
- DASHBOARD_ROOT = ".gitlab/dashboards"
-
class << self
def valid_params?(params)
params[:dashboard_path].present?
end
def all_dashboard_paths(project)
- file_finder(project)
- .list_files_for(DASHBOARD_ROOT)
+ project.repository.user_defined_metrics_dashboard_paths
.map do |filepath|
{
path: filepath,
@@ -27,13 +24,9 @@ module Metrics
end
end
- def file_finder(project)
- Gitlab::Template::Finders::RepoTemplateFinder.new(project, DASHBOARD_ROOT, '.yml')
- end
-
# Grabs the filepath after the base directory.
def name_for_path(filepath)
- filepath.delete_prefix("#{DASHBOARD_ROOT}/")
+ filepath.delete_prefix("#{Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT}/")
end
end
@@ -41,7 +34,7 @@ module Metrics
# Searches the project repo for a custom-defined dashboard.
def get_raw_dashboard
- yml = self.class.file_finder(project).read(dashboard_path)
+ yml = Gitlab::Metrics::Dashboard::RepoDashboardFinder.read_dashboard(project, dashboard_path)
load_yaml(yml)
end
diff --git a/app/services/metrics/dashboard/custom_metric_embed_service.rb b/app/services/metrics/dashboard/custom_metric_embed_service.rb
index 22b592c7aa5..229bd17f5cf 100644
--- a/app/services/metrics/dashboard/custom_metric_embed_service.rb
+++ b/app/services/metrics/dashboard/custom_metric_embed_service.rb
@@ -75,7 +75,6 @@ module Metrics
def panels
[{
type: DEFAULT_PANEL_TYPE,
- weight: DEFAULT_PANEL_WEIGHT,
title: title,
y_label: y_label,
metrics: metrics.map(&:to_metric_hash)
diff --git a/app/services/metrics/dashboard/dynamic_embed_service.rb b/app/services/metrics/dashboard/dynamic_embed_service.rb
index ff540c30579..0b198ecbbe9 100644
--- a/app/services/metrics/dashboard/dynamic_embed_service.rb
+++ b/app/services/metrics/dashboard/dynamic_embed_service.rb
@@ -18,7 +18,7 @@ module Metrics
# Determines whether the provided params are sufficient
# to uniquely identify a panel from a yml-defined dashboard.
#
- # See https://docs.gitlab.com/ee/user/project/integrations/prometheus.html#defining-custom-dashboards-per-project
+ # See https://docs.gitlab.com/ee/operations/metrics/dashboards/index.html#defining-custom-dashboards-per-project
# for additional info on defining custom dashboards.
def valid_params?(params)
[
diff --git a/app/services/metrics/dashboard/gitlab_alert_embed_service.rb b/app/services/metrics/dashboard/gitlab_alert_embed_service.rb
index 08d65413e1d..33c93b25c71 100644
--- a/app/services/metrics/dashboard/gitlab_alert_embed_service.rb
+++ b/app/services/metrics/dashboard/gitlab_alert_embed_service.rb
@@ -8,6 +8,7 @@
module Metrics
module Dashboard
class GitlabAlertEmbedService < ::Metrics::Dashboard::BaseEmbedService
+ include Gitlab::Metrics::Dashboard::Defaults
include Gitlab::Utils::StrongMemoize
SEQUENCE = [
@@ -63,7 +64,8 @@ module Metrics
{
title: prometheus_metric.title,
y_label: prometheus_metric.y_label,
- metrics: [prometheus_metric.to_metric_hash]
+ metrics: [prometheus_metric.to_metric_hash],
+ type: DEFAULT_PANEL_TYPE
}
end
diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
index 8e72a185406..b8c5c17c738 100644
--- a/app/services/metrics/dashboard/grafana_metric_embed_service.rb
+++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
@@ -33,7 +33,7 @@ module Metrics
def from_cache(project_id, user_id, grafana_url)
project = Project.find(project_id)
- user = User.find(user_id)
+ user = User.find(user_id) if user_id.present?
new(project, user, grafana_url: grafana_url)
end
@@ -56,7 +56,7 @@ module Metrics
end
def cache_key(*args)
- [project.id, current_user.id, grafana_url]
+ [project.id, current_user&.id, grafana_url]
end
# Required for ReactiveCaching; Usage overridden by
diff --git a/app/services/metrics/dashboard/panel_preview_service.rb b/app/services/metrics/dashboard/panel_preview_service.rb
new file mode 100644
index 00000000000..5b24d817fb6
--- /dev/null
+++ b/app/services/metrics/dashboard/panel_preview_service.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+# Ingest YAML fragment with metrics dashboard panel definition
+# https://docs.gitlab.com/ee/operations/metrics/dashboards/yaml.html#panel-panels-properties
+# process it and returns renderable json version
+module Metrics
+ module Dashboard
+ class PanelPreviewService
+ SEQUENCE = [
+ ::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter,
+ ::Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter,
+ ::Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter,
+ ::Gitlab::Metrics::Dashboard::Stages::AlertsInserter,
+ ::Gitlab::Metrics::Dashboard::Stages::UrlValidator
+ ].freeze
+
+ HANDLED_PROCESSING_ERRORS = [
+ Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError,
+ Gitlab::Config::Loader::Yaml::NotHashError,
+ Gitlab::Config::Loader::Yaml::DataTooLargeError,
+ Gitlab::Config::Loader::FormatError
+ ].freeze
+
+ def initialize(project, panel_yaml, environment)
+ @project, @panel_yaml, @environment = project, panel_yaml, environment
+ end
+
+ def execute
+ dashboard = ::Gitlab::Metrics::Dashboard::Processor.new(project, dashboard_structure, SEQUENCE, environment: environment).process
+ ServiceResponse.success(payload: dashboard[:panel_groups][0][:panels][0])
+ rescue *HANDLED_PROCESSING_ERRORS => error
+ ServiceResponse.error(message: error.message)
+ end
+
+ private
+
+ attr_accessor :project, :panel_yaml, :environment
+
+ def dashboard_structure
+ {
+ panel_groups: [
+ {
+ panels: [panel_hash]
+ }
+ ]
+ }
+ end
+
+ def panel_hash
+ ::Gitlab::Config::Loader::Yaml.new(panel_yaml).load_raw!
+ end
+ end
+ end
+end
diff --git a/app/services/metrics/dashboard/pod_dashboard_service.rb b/app/services/metrics/dashboard/pod_dashboard_service.rb
index 8699189deac..c83f8618460 100644
--- a/app/services/metrics/dashboard/pod_dashboard_service.rb
+++ b/app/services/metrics/dashboard/pod_dashboard_service.rb
@@ -4,10 +4,28 @@ module Metrics
module Dashboard
class PodDashboardService < ::Metrics::Dashboard::PredefinedDashboardService
DASHBOARD_PATH = 'config/prometheus/pod_metrics.yml'
- DASHBOARD_NAME = 'Pod Health'
+ DASHBOARD_NAME = N_('K8s pod health')
# SHA256 hash of dashboard content
- DASHBOARD_VERSION = 'f12f641d2575d5dcb69e2c633ff5231dbd879ad35020567d8fc4e1090bfdb4b4'
+ DASHBOARD_VERSION = '3a91b32f91b2dd3d90275333c0ea3630b3f3f37c4296ede5b5eef59bf523d66b'
+
+ SEQUENCE = [
+ STAGES::MetricEndpointInserter,
+ STAGES::VariableEndpointInserter,
+ STAGES::PanelIdsInserter
+ ].freeze
+
+ class << self
+ def all_dashboard_paths(_project)
+ [{
+ path: DASHBOARD_PATH,
+ display_name: _(DASHBOARD_NAME),
+ default: false,
+ system_dashboard: false,
+ out_of_the_box_dashboard: out_of_the_box_dashboard?
+ }]
+ end
+ end
private
diff --git a/app/services/metrics/dashboard/predefined_dashboard_service.rb b/app/services/metrics/dashboard/predefined_dashboard_service.rb
index c21083475f0..abdef66c2e0 100644
--- a/app/services/metrics/dashboard/predefined_dashboard_service.rb
+++ b/app/services/metrics/dashboard/predefined_dashboard_service.rb
@@ -12,8 +12,7 @@ module Metrics
SEQUENCE = [
STAGES::MetricEndpointInserter,
STAGES::VariableEndpointInserter,
- STAGES::PanelIdsInserter,
- STAGES::Sorter
+ STAGES::PanelIdsInserter
].freeze
class << self
@@ -30,6 +29,11 @@ module Metrics
end
end
+ # Returns an un-processed dashboard from the cache.
+ def raw_dashboard
+ Gitlab::Metrics::Dashboard::Cache.fetch(cache_key) { get_raw_dashboard }
+ end
+
private
def dashboard_version
diff --git a/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb b/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb
index f1f5cd7d77e..0651e569d07 100644
--- a/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb
+++ b/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb
@@ -6,17 +6,16 @@ module Metrics
module Dashboard
class SelfMonitoringDashboardService < ::Metrics::Dashboard::PredefinedDashboardService
DASHBOARD_PATH = 'config/prometheus/self_monitoring_default.yml'
- DASHBOARD_NAME = N_('Default dashboard')
+ DASHBOARD_NAME = N_('Overview')
# SHA256 hash of dashboard content
- DASHBOARD_VERSION = '1dff3e3cb76e73c8e368823c98b34c61aec0d141978450dea195a3b3dc2415d6'
+ DASHBOARD_VERSION = '0f7ade2022e09f1a1da8e883cc95d84b9557e1e0e9b015c51eb964296aa73098'
SEQUENCE = [
STAGES::CustomMetricsInserter,
STAGES::MetricEndpointInserter,
STAGES::VariableEndpointInserter,
- STAGES::PanelIdsInserter,
- STAGES::Sorter
+ STAGES::PanelIdsInserter
].freeze
class << self
@@ -29,7 +28,7 @@ module Metrics
path: DASHBOARD_PATH,
display_name: _(DASHBOARD_NAME),
default: true,
- system_dashboard: false,
+ system_dashboard: true,
out_of_the_box_dashboard: out_of_the_box_dashboard?
}]
end
diff --git a/app/services/metrics/dashboard/system_dashboard_service.rb b/app/services/metrics/dashboard/system_dashboard_service.rb
index 5c3562b8ca0..29b8f23f40d 100644
--- a/app/services/metrics/dashboard/system_dashboard_service.rb
+++ b/app/services/metrics/dashboard/system_dashboard_service.rb
@@ -6,10 +6,10 @@ module Metrics
module Dashboard
class SystemDashboardService < ::Metrics::Dashboard::PredefinedDashboardService
DASHBOARD_PATH = 'config/prometheus/common_metrics.yml'
- DASHBOARD_NAME = N_('Default dashboard')
+ DASHBOARD_NAME = N_('Overview')
# SHA256 hash of dashboard content
- DASHBOARD_VERSION = '4685fe386c25b1a786b3be18f79bb2ee9828019003e003816284cdb634fa3e13'
+ DASHBOARD_VERSION = 'ce9ae27d2913f637de851d61099bc4151583eae68b1386a2176339ef6e653223'
SEQUENCE = [
STAGES::CommonMetricsInserter,
@@ -18,7 +18,6 @@ module Metrics
STAGES::MetricEndpointInserter,
STAGES::VariableEndpointInserter,
STAGES::PanelIdsInserter,
- STAGES::Sorter,
STAGES::AlertsInserter
].freeze
diff --git a/app/services/metrics/dashboard/update_dashboard_service.rb b/app/services/metrics/dashboard/update_dashboard_service.rb
index d37d06a0222..d990e96ecb5 100644
--- a/app/services/metrics/dashboard/update_dashboard_service.rb
+++ b/app/services/metrics/dashboard/update_dashboard_service.rb
@@ -7,7 +7,7 @@ module Metrics
include Stepable
ALLOWED_FILE_TYPE = '.yml'
- USER_DASHBOARDS_DIR = ::Metrics::Dashboard::CustomDashboardService::DASHBOARD_ROOT
+ USER_DASHBOARDS_DIR = ::Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT
steps :check_push_authorized,
:check_branch_name,
@@ -68,7 +68,7 @@ module Metrics
end
def push_authorized?
- Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch)
+ Gitlab::UserAccess.new(current_user, container: project).can_push_to_branch?(branch)
end
def valid_branch_name?
diff --git a/app/services/notes/copy_service.rb b/app/services/notes/copy_service.rb
new file mode 100644
index 00000000000..6e5b4596602
--- /dev/null
+++ b/app/services/notes/copy_service.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+# This service copies Notes from one Noteable to another.
+#
+# It expects the calling code to have performed the necessary authorization
+# checks in order to allow the copy to happen.
+module Notes
+ class CopyService
+ def initialize(current_user, from_noteable, to_noteable)
+ raise ArgumentError, 'Noteables must be different' if from_noteable == to_noteable
+
+ @current_user = current_user
+ @from_noteable = from_noteable
+ @to_noteable = to_noteable
+ @from_project = from_noteable.project
+ @new_discussion_ids = {}
+ end
+
+ def execute
+ from_noteable.notes_with_associations.find_each do |note|
+ copy_note(note)
+ end
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_reader :from_noteable, :to_noteable, :from_project, :current_user, :new_discussion_ids
+
+ def copy_note(note)
+ new_note = note.dup
+ new_params = params_from_note(note, new_note)
+ new_note.update!(new_params)
+
+ copy_award_emoji(note, new_note)
+ end
+
+ def params_from_note(note, new_note)
+ new_discussion_ids[note.discussion_id] ||= Discussion.discussion_id(new_note)
+ rewritten_note = MarkdownContentRewriterService.new(current_user, note.note, from_project, to_noteable.resource_parent).execute
+
+ new_params = {
+ project: to_noteable.project,
+ noteable: to_noteable,
+ discussion_id: new_discussion_ids[note.discussion_id],
+ note: rewritten_note,
+ note_html: nil,
+ created_at: note.created_at,
+ updated_at: note.updated_at
+ }
+
+ if note.system_note_metadata
+ new_params[:system_note_metadata] = note.system_note_metadata.dup
+
+ # TODO: Implement copying of description versions when an issue is moved
+ # https://gitlab.com/gitlab-org/gitlab/issues/32300
+ new_params[:system_note_metadata].description_version = nil
+ end
+
+ new_params
+ end
+
+ def copy_award_emoji(from_note, to_note)
+ AwardEmojis::CopyService.new(from_note, to_note).execute
+ end
+ end
+end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 935dbfb72dd..4f2329a42f2 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -54,7 +54,8 @@ module Notes
def when_saved(note)
if note.part_of_discussion? && note.discussion.can_convert_to_discussion?
- note.discussion.convert_to_discussion!(save: true)
+ note.discussion.convert_to_discussion!.save
+ note.clear_memoization(:discussion)
end
todo_service.new_note(note, current_user)
@@ -66,13 +67,13 @@ module Notes
Gitlab::Tracking.event('Notes::CreateService', 'execute', tracking_data_for(note))
end
- if Feature.enabled?(:merge_ref_head_comments, project) && note.for_merge_request? && note.diff_note? && note.start_of_discussion?
+ if Feature.enabled?(:merge_ref_head_comments, project, default_enabled: true) && note.for_merge_request? && note.diff_note? && note.start_of_discussion?
Discussions::CaptureDiffNotePositionService.new(note.noteable, note.diff_file&.paths).execute(note.discussion)
end
end
def do_commands(note, update_params, message, only_commands)
- return if quick_actions_service.commands_executed_count.to_i.zero?
+ return if quick_actions_service.commands_executed_count.to_i == 0
if update_params.present?
quick_actions_service.apply_updates(update_params, note)
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index c670f01e502..36d9f1d7867 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -50,6 +50,11 @@ module Notes
return if update_params.empty?
return unless supported?(note)
+ # We need the `id` after the note is persisted
+ if update_params[:spend_time]
+ update_params[:spend_time][:note_id] = note.id
+ end
+
self.class.noteable_update_service(note).new(note.resource_parent, current_user, update_params).execute(note.noteable)
end
end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 047848fd1a3..193d3080078 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -25,7 +25,7 @@ module Notes
note.note = content
end
- unless only_commands
+ unless only_commands || note.for_personal_snippet?
note.create_new_cross_references!(current_user)
update_todos(note, old_mentioned_users)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index a4e935a8cf5..909a0033d12 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -66,6 +66,13 @@ class NotificationService
mailer.access_token_about_to_expire_email(user).deliver_later
end
+ # Notify the user when at least one of their personal access tokens has expired today
+ def access_token_expired(user)
+ return unless user.can?(:receive_notifications)
+
+ mailer.access_token_expired_email(user).deliver_later
+ end
+
# Notify a user when a previously unknown IP or device is used to
# sign in to their account
def unknown_sign_in(user, ip, time)
@@ -424,8 +431,8 @@ class NotificationService
end
def project_was_moved(project, old_path_with_namespace)
- recipients = project.private? ? project.team.members_in_project_and_ancestors : project.team.members
- recipients = notifiable_users(recipients, :mention, project: project)
+ recipients = project_moved_recipients(project)
+ recipients = notifiable_users(recipients, :custom, custom_action: :moved_project, project: project)
recipients.each do |recipient|
mailer.project_was_moved_email(
@@ -705,6 +712,14 @@ class NotificationService
recipients
end
+ def project_moved_recipients(project)
+ finder = MembersFinder.new(project, nil, params: {
+ active_without_invites_and_requests: true,
+ owners_and_maintainers: true
+ })
+ finder.execute.preload_user_and_notification_settings.map(&:user)
+ end
+
def project_maintainers_recipients(target, action:)
NotificationRecipients::BuildService.build_project_maintainers_recipients(target, action: action)
end
diff --git a/app/services/packages/maven/find_or_create_package_service.rb b/app/services/packages/maven/find_or_create_package_service.rb
index 50a008843ad..505f45a7b21 100644
--- a/app/services/packages/maven/find_or_create_package_service.rb
+++ b/app/services/packages/maven/find_or_create_package_service.rb
@@ -3,21 +3,33 @@ module Packages
module Maven
class FindOrCreatePackageService < BaseService
MAVEN_METADATA_FILE = 'maven-metadata.xml'.freeze
+ SNAPSHOT_TERM = '-SNAPSHOT'.freeze
def execute
- package = ::Packages::Maven::PackageFinder
- .new(params[:path], current_user, project: project).execute
+ package =
+ ::Packages::Maven::PackageFinder.new(params[:path], current_user, project: project)
+ .execute
unless package
- if params[:file_name] == MAVEN_METADATA_FILE
- # Maven uploads several files during `mvn deploy` in next order:
- # - my-company/my-app/1.0-SNAPSHOT/my-app.jar
- # - my-company/my-app/1.0-SNAPSHOT/my-app.pom
- # - my-company/my-app/1.0-SNAPSHOT/maven-metadata.xml
- # - my-company/my-app/maven-metadata.xml
- #
- # The last xml file does not have VERSION in URL because it contains
- # information about all versions.
+ # Maven uploads several files during `mvn deploy` in next order:
+ # - my-company/my-app/1.0-SNAPSHOT/my-app.jar
+ # - my-company/my-app/1.0-SNAPSHOT/my-app.pom
+ # - my-company/my-app/1.0-SNAPSHOT/maven-metadata.xml
+ # - my-company/my-app/maven-metadata.xml
+ #
+ # The last xml file does not have VERSION in URL because it contains
+ # information about all versions. When uploading such file, we create
+ # a package with a version set to `nil`. The xml file with a version
+ # is only created and uploaded for snapshot versions.
+ #
+ # Gradle has a different upload order:
+ # - my-company/my-app/1.0-SNAPSHOT/maven-metadata.xml
+ # - my-company/my-app/1.0-SNAPSHOT/my-app.jar
+ # - my-company/my-app/1.0-SNAPSHOT/my-app.pom
+ # - my-company/my-app/maven-metadata.xml
+ #
+ # The first upload has to create the proper package (the one with the version set).
+ if params[:file_name] == MAVEN_METADATA_FILE && !params[:path]&.ends_with?(SNAPSHOT_TERM)
package_name, version = params[:path], nil
else
package_name, _, version = params[:path].rpartition('/')
@@ -30,8 +42,9 @@ module Packages
build: params[:build]
}
- package = ::Packages::Maven::CreatePackageService
- .new(project, current_user, package_params).execute
+ package =
+ ::Packages::Maven::CreatePackageService.new(project, current_user, package_params)
+ .execute
end
package
diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb
index 6fec398fab0..59125669f7d 100644
--- a/app/services/packages/nuget/metadata_extraction_service.rb
+++ b/app/services/packages/nuget/metadata_extraction_service.rb
@@ -42,7 +42,7 @@ module Packages
def valid_package_file?
package_file &&
package_file.package&.nuget? &&
- package_file.file.size.positive?
+ package_file.file.size > 0 # rubocop:disable Style/ZeroLengthPredicate
end
def extract_metadata(file)
diff --git a/app/services/packages/nuget/search_service.rb b/app/services/packages/nuget/search_service.rb
index f7e09e11819..b95aa30bec1 100644
--- a/app/services/packages/nuget/search_service.rb
+++ b/app/services/packages/nuget/search_service.rb
@@ -21,8 +21,8 @@ module Packages
@search_term = search_term
@options = DEFAULT_OPTIONS.merge(options)
- raise ArgumentError, 'negative per_page' if per_page.negative?
- raise ArgumentError, 'negative padding' if padding.negative?
+ raise ArgumentError, 'negative per_page' if per_page < 0
+ raise ArgumentError, 'negative padding' if padding < 0
end
def execute
diff --git a/app/services/personal_access_tokens/revoke_service.rb b/app/services/personal_access_tokens/revoke_service.rb
new file mode 100644
index 00000000000..16ba42bd317
--- /dev/null
+++ b/app/services/personal_access_tokens/revoke_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module PersonalAccessTokens
+ class RevokeService
+ attr_reader :token, :current_user
+
+ def initialize(current_user = nil, params = { token: nil })
+ @current_user = current_user
+ @token = params[:token]
+ end
+
+ def execute
+ return ServiceResponse.error(message: 'Not permitted to revoke') unless revocation_permitted?
+
+ if token.revoke!
+ ServiceResponse.success(message: success_message)
+ else
+ ServiceResponse.error(message: error_message)
+ end
+ end
+
+ private
+
+ def error_message
+ _("Could not revoke personal access token %{personal_access_token_name}.") % { personal_access_token_name: token.name }
+ end
+
+ def success_message
+ _("Revoked personal access token %{personal_access_token_name}!") % { personal_access_token_name: token.name }
+ end
+
+ def revocation_permitted?
+ Ability.allowed?(current_user, :revoke_token, token)
+ end
+ end
+end
diff --git a/app/services/product_analytics/build_graph_service.rb b/app/services/product_analytics/build_graph_service.rb
new file mode 100644
index 00000000000..31f9f093bb9
--- /dev/null
+++ b/app/services/product_analytics/build_graph_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module ProductAnalytics
+ class BuildGraphService
+ def initialize(project, params)
+ @project = project
+ @params = params
+ end
+
+ def execute
+ graph = @params[:graph].to_sym
+ timerange = @params[:timerange].days
+
+ results = product_analytics_events.count_by_graph(graph, timerange)
+
+ {
+ id: graph,
+ keys: results.keys,
+ values: results.values
+ }
+ end
+
+ private
+
+ def product_analytics_events
+ @project.product_analytics_events
+ end
+ end
+end
diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb
index e08bc8efb15..f883c8c7bd8 100644
--- a/app/services/projects/alerting/notify_service.rb
+++ b/app/services/projects/alerting/notify_service.rb
@@ -58,10 +58,6 @@ module Projects
AlertManagement::Alert.not_resolved.for_fingerprint(project, fingerprint).first
end
- def send_email?
- incident_management_setting.send_email?
- end
-
def process_incident_issues(alert)
return if alert.issue
diff --git a/app/services/projects/auto_devops/disable_service.rb b/app/services/projects/auto_devops/disable_service.rb
index c90510c581d..e10668ac9bd 100644
--- a/app/services/projects/auto_devops/disable_service.rb
+++ b/app/services/projects/auto_devops/disable_service.rb
@@ -23,7 +23,7 @@ module Projects
# for more context.
# rubocop: disable CodeReuse/ActiveRecord
def first_pipeline_failure?
- auto_devops_pipelines.success.limit(1).count.zero? &&
+ auto_devops_pipelines.success.limit(1).count == 0 &&
auto_devops_pipelines.failed.limit(1).count.nonzero?
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/projects/cleanup_service.rb b/app/services/projects/cleanup_service.rb
index 04624b96bf0..4ced9feff00 100644
--- a/app/services/projects/cleanup_service.rb
+++ b/app/services/projects/cleanup_service.rb
@@ -22,7 +22,7 @@ module Projects
apply_bfg_object_map!
# Remove older objects that are no longer referenced
- GitGarbageCollectWorker.new.perform(project.id, :gc)
+ GitGarbageCollectWorker.new.perform(project.id, :gc, "project_cleanup:gc:#{project.id}")
# The cache may now be inaccurate, and holding onto it could prevent
# bugs assuming the presence of some object from manifesting for some
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
index c5809c11ea9..204a54ff23a 100644
--- a/app/services/projects/container_repository/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -39,11 +39,8 @@ module Projects
end
def filter_by_name(tags)
- # Technical Debt: https://gitlab.com/gitlab-org/gitlab/issues/207267
- # name_regex to be removed when container_expiration_policies is updated
- # to have both regex columns
- regex_delete = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_delete'] || params['name_regex']}\\z")
- regex_retain = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_keep']}\\z")
+ regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_delete'] || params['name_regex']}\\z")
+ regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_keep']}\\z")
tags.select do |tag|
# regex_retain will override any overlapping matches by regex_delete
@@ -81,11 +78,11 @@ module Projects
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?
+ ::Gitlab::UntrustedRegexp.new(regex) unless regex.blank?
end
true
rescue RegexpError => e
- Gitlab::ErrorTracking.log_exception(e, project_id: project.id)
+ ::Gitlab::ErrorTracking.log_exception(e, project_id: project.id)
false
end
end
diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb
index 5d4059710bb..a23a6a369b2 100644
--- a/app/services/projects/container_repository/delete_tags_service.rb
+++ b/app/services/projects/container_repository/delete_tags_service.rb
@@ -6,65 +6,35 @@ module Projects
LOG_DATA_BASE = { service_class: self.to_s }.freeze
def execute(container_repository)
+ @container_repository = container_repository
return error('access denied') unless can?(current_user, :destroy_container_image, project)
- tag_names = params[:tags]
- return error('not tags specified') if tag_names.blank?
+ @tag_names = params[:tags]
+ return error('not tags specified') if @tag_names.blank?
- smart_delete(container_repository, tag_names)
+ delete_tags
end
private
- # Delete tags by name with a single DELETE request. This is only supported
- # by the GitLab Container Registry fork. See
- # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23325 for details.
- def fast_delete(container_repository, tag_names)
- deleted_tags = tag_names.select do |name|
- container_repository.delete_tag_by_name(name)
- end
-
- deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags')
+ def delete_tags
+ delete_service.execute
+ .tap(&method(:log_response))
end
- # Replace a tag on the registry with a dummy tag.
- # This is a hack as the registry doesn't support deleting individual
- # tags. This code effectively pushes a dummy image and assigns the tag to it.
- # This way when the tag is deleted only the dummy image is affected.
- # This is used to preverse compatibility with third-party registries that
- # don't support fast delete.
- # See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion
- def slow_delete(container_repository, tag_names)
- # generates the blobs for the dummy image
- dummy_manifest = container_repository.client.generate_empty_manifest(container_repository.path)
- return error('could not generate manifest') if dummy_manifest.nil?
-
- deleted_tags = replace_tag_manifests(container_repository, dummy_manifest, tag_names)
+ def delete_service
+ fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true)
- # Deletes the dummy image
- # All created tag digests are the same since they all have the same dummy image.
- # a single delete is sufficient to remove all tags with it
- if deleted_tags.any? && container_repository.delete_tag_by_digest(deleted_tags.each_value.first)
- success(deleted: deleted_tags.keys)
+ if fast_delete_enabled && @container_repository.client.supports_tag_delete?
+ ::Projects::ContainerRepository::Gitlab::DeleteTagsService.new(@container_repository, @tag_names)
else
- error('could not delete tags')
+ ::Projects::ContainerRepository::ThirdParty::DeleteTagsService.new(@container_repository, @tag_names)
end
end
- def smart_delete(container_repository, tag_names)
- fast_delete_enabled = Feature.enabled?(:container_registry_fast_tag_delete, default_enabled: true)
- response = if fast_delete_enabled && container_repository.client.supports_tag_delete?
- fast_delete(container_repository, tag_names)
- else
- slow_delete(container_repository, tag_names)
- end
-
- response.tap { |r| log_response(r, container_repository) }
- end
-
- def log_response(response, container_repository)
+ def log_response(response)
log_data = LOG_DATA_BASE.merge(
- container_repository_id: container_repository.id,
+ container_repository_id: @container_repository.id,
message: 'deleted tags'
)
@@ -76,26 +46,6 @@ module Projects
log_error(log_data)
end
end
-
- # update the manifests of the tags with the new dummy image
- def replace_tag_manifests(container_repository, dummy_manifest, tag_names)
- deleted_tags = {}
-
- tag_names.each do |name|
- digest = container_repository.client.put_tag(container_repository.path, name, dummy_manifest)
- next unless digest
-
- deleted_tags[name] = digest
- end
-
- # make sure the digests are the same (it should always be)
- digests = deleted_tags.values.uniq
-
- # rubocop: disable CodeReuse/ActiveRecord
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new('multiple tag digests')) if digests.many?
-
- deleted_tags
- end
end
end
end
diff --git a/app/services/projects/container_repository/gitlab/delete_tags_service.rb b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
new file mode 100644
index 00000000000..18049648e26
--- /dev/null
+++ b/app/services/projects/container_repository/gitlab/delete_tags_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Projects
+ module ContainerRepository
+ module Gitlab
+ class DeleteTagsService
+ include BaseServiceUtility
+
+ def initialize(container_repository, tag_names)
+ @container_repository = container_repository
+ @tag_names = tag_names
+ end
+
+ # Delete tags by name with a single DELETE request. This is only supported
+ # by the GitLab Container Registry fork. See
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23325 for details.
+ def execute
+ return success(deleted: []) if @tag_names.empty?
+
+ deleted_tags = @tag_names.select do |name|
+ @container_repository.delete_tag_by_name(name)
+ end
+
+ deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags')
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/projects/container_repository/third_party/delete_tags_service.rb b/app/services/projects/container_repository/third_party/delete_tags_service.rb
new file mode 100644
index 00000000000..6504172109e
--- /dev/null
+++ b/app/services/projects/container_repository/third_party/delete_tags_service.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module Projects
+ module ContainerRepository
+ module ThirdParty
+ class DeleteTagsService
+ include BaseServiceUtility
+
+ def initialize(container_repository, tag_names)
+ @container_repository = container_repository
+ @tag_names = tag_names
+ end
+
+ # Replace a tag on the registry with a dummy tag.
+ # This is a hack as the registry doesn't support deleting individual
+ # tags. This code effectively pushes a dummy image and assigns the tag to it.
+ # This way when the tag is deleted only the dummy image is affected.
+ # This is used to preverse compatibility with third-party registries that
+ # don't support fast delete.
+ # See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion
+ def execute
+ return success(deleted: []) if @tag_names.empty?
+
+ # generates the blobs for the dummy image
+ dummy_manifest = @container_repository.client.generate_empty_manifest(@container_repository.path)
+ return error('could not generate manifest') if dummy_manifest.nil?
+
+ deleted_tags = replace_tag_manifests(dummy_manifest)
+
+ # Deletes the dummy image
+ # All created tag digests are the same since they all have the same dummy image.
+ # a single delete is sufficient to remove all tags with it
+ if deleted_tags.any? && @container_repository.delete_tag_by_digest(deleted_tags.each_value.first)
+ success(deleted: deleted_tags.keys)
+ else
+ error('could not delete tags')
+ end
+ end
+
+ private
+
+ # update the manifests of the tags with the new dummy image
+ def replace_tag_manifests(dummy_manifest)
+ deleted_tags = {}
+
+ @tag_names.each do |name|
+ digest = @container_repository.client.put_tag(@container_repository.path, name, dummy_manifest)
+ next unless digest
+
+ deleted_tags[name] = digest
+ end
+
+ # make sure the digests are the same (it should always be)
+ digests = deleted_tags.values.uniq
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new('multiple tag digests')) if digests.many?
+
+ deleted_tags
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 6569277ad9d..33ed1151407 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -55,9 +55,11 @@ module Projects
save_project_and_import_data
- after_create_actions if @project.persisted?
+ Gitlab::ApplicationContext.with_context(related_class: "Projects::CreateService", project: @project) do
+ after_create_actions if @project.persisted?
- import_schedule
+ import_schedule
+ end
@project
rescue ActiveRecord::RecordInvalid => e
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 2e949f2fc55..37487261f2c 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -31,7 +31,7 @@ module Projects
attempt_destroy_transaction(project)
system_hook_service.execute_hooks_for(project, :destroy)
- log_info("Project \"#{project.full_path}\" was removed")
+ log_info("Project \"#{project.full_path}\" was deleted")
current_user.invalidate_personal_projects_count
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 6ac53b15ef9..bb660d47887 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -14,10 +14,10 @@ module Projects
@valid_fork_targets ||= ForkTargetsFinder.new(@project, current_user).execute
end
- def valid_fork_target?
+ def valid_fork_target?(namespace = target_namespace)
return true if current_user.admin?
- valid_fork_targets.include?(target_namespace)
+ valid_fork_targets.include?(namespace)
end
private
diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb
index ea557ebe20f..d32ead76d00 100644
--- a/app/services/projects/prometheus/alerts/notify_service.rb
+++ b/app/services/projects/prometheus/alerts/notify_service.rb
@@ -42,10 +42,6 @@ module Projects
Gitlab::Utils::DeepSize.new(params).valid?
end
- def send_email?
- incident_management_setting.send_email && firings.any?
- end
-
def firings
@firings ||= alerts_by_status('firing')
end
@@ -125,6 +121,8 @@ module Projects
end
def send_alert_email
+ return unless firings.any?
+
notification_service
.async
.prometheus_alerts_fired(project, firings)
diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_service_template.rb
index b6465810fde..54d09b354a1 100644
--- a/app/services/projects/propagate_service_template.rb
+++ b/app/services/projects/propagate_service_template.rb
@@ -66,7 +66,7 @@ module Projects
# rubocop: disable CodeReuse/ActiveRecord
def run_callbacks(batch)
- if active_external_issue_tracker?
+ if template.issue_tracker?
Project.where(id: batch).update_all(has_external_issue_tracker: true)
end
@@ -76,10 +76,6 @@ module Projects
end
# rubocop: enable CodeReuse/ActiveRecord
- def active_external_issue_tracker?
- template.issue_tracker? && !template.default
- end
-
def active_external_wiki?
template.type == 'ExternalWikiService'
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 60e5b7e2639..0fb70feec86 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -55,10 +55,18 @@ module Projects
raise TransferError.new(s_('TransferProject|Project cannot be transferred, because tags are present in its container registry'))
end
+ if project.has_packages?(:npm) && !new_namespace_has_same_root?(project)
+ raise TransferError.new(s_("TransferProject|Root namespace can't be updated if project has NPM packages"))
+ end
+
attempt_transfer_transaction
end
# rubocop: enable CodeReuse/ActiveRecord
+ def new_namespace_has_same_root?(project)
+ new_namespace.root_ancestor == project.namespace.root_ancestor
+ end
+
def attempt_transfer_transaction
Project.transaction do
project.expire_caches_before_rename(@old_path)
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
index 674071ad92a..88c17d502df 100644
--- a/app/services/projects/update_pages_configuration_service.rb
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -11,15 +11,20 @@ module Projects
end
def execute
- if file_equals?(pages_config_file, pages_config_json)
- return success(reload: false)
+ # If the pages were never deployed, we can't write out the config, as the
+ # directory would not exist.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/235139
+ return success unless project.pages_deployed?
+
+ unless file_equals?(pages_config_file, pages_config_json)
+ update_file(pages_config_file, pages_config_json)
+ reload_daemon
end
- update_file(pages_config_file, pages_config_json)
- reload_daemon
- success(reload: true)
+ success
rescue => e
- error(e.message)
+ Gitlab::ErrorTracking.track_exception(e)
+ error(e.message, pass_back: { exception: e })
end
private
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 59389a0fa65..334f5993d15 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -136,7 +136,7 @@ module Projects
def max_size
max_pages_size = max_size_from_settings
- return ::Gitlab::Pages::MAX_SIZE if max_pages_size.zero?
+ return ::Gitlab::Pages::MAX_SIZE if max_pages_size == 0
max_pages_size
end
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
index d6c0d647468..fe2610f89fb 100644
--- a/app/services/projects/update_remote_mirror_service.rb
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -25,14 +25,8 @@ module Projects
def update_mirror(remote_mirror)
remote_mirror.update_start!
-
remote_mirror.ensure_remote!
- # https://gitlab.com/gitlab-org/gitaly/-/issues/2670
- if Feature.disabled?(:gitaly_ruby_remote_branches_ls_remote, default_enabled: true)
- repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true)
- end
-
response = remote_mirror.update_repository
if response.divergent_refs.any?
diff --git a/app/services/projects/update_repository_storage_service.rb b/app/services/projects/update_repository_storage_service.rb
index 7b346c09635..a479d53a43a 100644
--- a/app/services/projects/update_repository_storage_service.rb
+++ b/app/services/projects/update_repository_storage_service.rb
@@ -6,8 +6,7 @@ module Projects
SameFilesystemError = Class.new(Error)
attr_reader :repository_storage_move
- delegate :project, :destination_storage_name, to: :repository_storage_move
- delegate :repository, to: :project
+ delegate :project, :source_storage_name, :destination_storage_name, to: :repository_storage_move
def initialize(repository_storage_move)
@repository_storage_move = repository_storage_move
@@ -20,21 +19,22 @@ module Projects
repository_storage_move.start!
end
- raise SameFilesystemError if same_filesystem?(repository.storage, destination_storage_name)
+ raise SameFilesystemError if same_filesystem?(source_storage_name, destination_storage_name)
mirror_repositories
- project.transaction do
- mark_old_paths_for_archive
-
- repository_storage_move.finish!
+ repository_storage_move.transaction do
+ repository_storage_move.finish_replication!
project.leave_pool_repository
project.track_project_repository
end
+ remove_old_paths
enqueue_housekeeping
+ repository_storage_move.finish_cleanup!
+
ServiceResponse.success
rescue StandardError => e
@@ -91,36 +91,31 @@ module Projects
end
end
- def mark_old_paths_for_archive
- old_repository_storage = project.repository_storage
- new_project_path = moved_path(project.disk_path)
-
- # Notice that the block passed to `run_after_commit` will run with `repository_storage_move`
- # as its context
- repository_storage_move.run_after_commit do
- GitlabShellWorker.perform_async(:mv_repository,
- old_repository_storage,
- project.disk_path,
- new_project_path)
-
- if project.wiki.repository_exists?
- GitlabShellWorker.perform_async(:mv_repository,
- old_repository_storage,
- project.wiki.disk_path,
- "#{new_project_path}.wiki")
- end
-
- if project.design_repository.exists?
- GitlabShellWorker.perform_async(:mv_repository,
- old_repository_storage,
- project.design_repository.disk_path,
- "#{new_project_path}.design")
- end
+ def remove_old_paths
+ Gitlab::Git::Repository.new(
+ source_storage_name,
+ "#{project.disk_path}.git",
+ nil,
+ nil
+ ).remove
+
+ if project.wiki.repository_exists?
+ Gitlab::Git::Repository.new(
+ source_storage_name,
+ "#{project.wiki.disk_path}.git",
+ nil,
+ nil
+ ).remove
end
- end
- def moved_path(path)
- "#{path}+#{project.id}+moved+#{Time.current.to_i}"
+ if project.design_repository.exists?
+ Gitlab::Git::Repository.new(
+ source_storage_name,
+ "#{project.design_repository.disk_path}.git",
+ nil,
+ nil
+ ).remove
+ end
end
# The underlying FetchInternalRemote call uses a `git fetch` to move data
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 58c9bce963b..c9ba7cde199 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -142,7 +142,13 @@ module Projects
end
def update_pages_config
- Projects::UpdatePagesConfigurationService.new(project).execute
+ return unless project.pages_deployed?
+
+ if Feature.enabled?(:async_update_pages_config, project)
+ PagesUpdateConfigurationWorker.perform_async(project.id)
+ else
+ Projects::UpdatePagesConfigurationService.new(project).execute
+ end
end
def changing_pages_https_only?
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
index 2d0a78feb8e..c253154c1b7 100644
--- a/app/services/resource_access_tokens/create_service.rb
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -32,7 +32,9 @@ module ResourceAccessTokens
attr_reader :resource_type, :resource
def feature_enabled?
- ::Feature.enabled?(:resource_access_token, resource)
+ return false if ::Gitlab.com?
+
+ ::Feature.enabled?(:resource_access_token, resource, default_enabled: true)
end
def has_permission_to_create?
diff --git a/app/services/resource_events/base_change_timebox_service.rb b/app/services/resource_events/base_change_timebox_service.rb
new file mode 100644
index 00000000000..5c83f7b12f7
--- /dev/null
+++ b/app/services/resource_events/base_change_timebox_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module ResourceEvents
+ class BaseChangeTimeboxService
+ attr_reader :resource, :user, :event_created_at
+
+ def initialize(resource, user, created_at: Time.current)
+ @resource = resource
+ @user = user
+ @event_created_at = created_at
+ end
+
+ def execute
+ create_event
+
+ resource.expire_note_etag_cache
+ end
+
+ private
+
+ def create_event
+ raise NotImplementedError
+ end
+
+ def build_resource_args
+ key = resource.class.name.foreign_key
+
+ {
+ user_id: user.id,
+ created_at: event_created_at,
+ key => resource.id
+ }
+ end
+ end
+end
diff --git a/app/services/resource_events/change_milestone_service.rb b/app/services/resource_events/change_milestone_service.rb
index 82c3e2acad5..dcdf87599ac 100644
--- a/app/services/resource_events/change_milestone_service.rb
+++ b/app/services/resource_events/change_milestone_service.rb
@@ -1,37 +1,30 @@
# frozen_string_literal: true
module ResourceEvents
- class ChangeMilestoneService
- attr_reader :resource, :user, :event_created_at, :milestone, :old_milestone
+ class ChangeMilestoneService < BaseChangeTimeboxService
+ attr_reader :milestone, :old_milestone
def initialize(resource, user, created_at: Time.current, old_milestone:)
- @resource = resource
- @user = user
- @event_created_at = created_at
+ super(resource, user, created_at: created_at)
+
@milestone = resource&.milestone
@old_milestone = old_milestone
end
- def execute
- ResourceMilestoneEvent.create(build_resource_args)
+ private
- resource.expire_note_etag_cache
+ def create_event
+ ResourceMilestoneEvent.create(build_resource_args)
end
- private
-
def build_resource_args
action = milestone.blank? ? :remove : :add
- key = resource.class.name.foreign_key
- {
- user_id: user.id,
- created_at: event_created_at,
- milestone_id: action == :add ? milestone&.id : old_milestone&.id,
+ super.merge({
state: ResourceMilestoneEvent.states[resource.state],
- action: ResourceMilestoneEvent.actions[action],
- key => resource.id
- }
+ action: ResourceTimeboxEvent.actions[action],
+ milestone_id: milestone.blank? ? old_milestone&.id : milestone&.id
+ })
end
end
end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 650dc197f8c..278cf389e07 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -70,7 +70,7 @@ class SearchService
def per_page
per_page_param = params[:per_page].to_i
- return DEFAULT_PER_PAGE unless per_page_param.positive?
+ return DEFAULT_PER_PAGE unless per_page_param > 0
[MAX_PER_PAGE, per_page_param].min
end
diff --git a/app/services/service_desk_settings/update_service.rb b/app/services/service_desk_settings/update_service.rb
index 08106b04d18..c837b75f439 100644
--- a/app/services/service_desk_settings/update_service.rb
+++ b/app/services/service_desk_settings/update_service.rb
@@ -9,6 +9,8 @@ module ServiceDeskSettings
params.delete(:project_key)
end
+ params[:project_key] = nil if params[:project_key].blank?
+
if settings.update(params)
success
else
diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb
index 4bbde3a9648..9191943caa7 100644
--- a/app/services/submit_usage_ping_service.rb
+++ b/app/services/submit_usage_ping_service.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
class SubmitUsagePingService
- URL = 'https://version.gitlab.com/usage_data'
+ PRODUCTION_URL = 'https://version.gitlab.com/usage_data'
+ STAGING_URL = 'https://gitlab-services-version-gitlab-com-staging.gs-staging.gitlab.org/usage_data'
METRICS = %w[leader_issues instance_issues percentage_issues leader_notes instance_notes
percentage_notes leader_milestones instance_milestones percentage_milestones
@@ -13,28 +14,42 @@ class SubmitUsagePingService
percentage_projects_prometheus_active leader_service_desk_issues instance_service_desk_issues
percentage_service_desk_issues].freeze
+ SubmissionError = Class.new(StandardError)
+
def execute
- return false unless Gitlab::CurrentSettings.usage_ping_enabled?
- return false if User.single_user&.requires_usage_stats_consent?
+ return unless Gitlab::CurrentSettings.usage_ping_enabled?
+ return if User.single_user&.requires_usage_stats_consent?
+
+ usage_data = Gitlab::UsageData.data(force_refresh: true)
+
+ raise SubmissionError.new('Usage data is blank') if usage_data.blank?
+
+ raw_usage_data = save_raw_usage_data(usage_data)
response = Gitlab::HTTP.post(
- URL,
- body: Gitlab::UsageData.to_json(force_refresh: true),
+ url,
+ body: usage_data.to_json,
allow_local_requests: true,
headers: { 'Content-type' => 'application/json' }
)
- store_metrics(response)
+ raise SubmissionError.new("Unsuccessful response code: #{response.code}") unless response.success?
- true
- rescue Gitlab::HTTP::Error => e
- Gitlab::AppLogger.info("Unable to contact GitLab, Inc.: #{e}")
+ raw_usage_data.update_sent_at! if raw_usage_data
- false
+ store_metrics(response)
end
private
+ def save_raw_usage_data(usage_data)
+ return unless Feature.enabled?(:save_raw_usage_data)
+
+ RawUsageData.safe_find_or_create_by(recorded_at: usage_data[:recorded_at]) do |record|
+ record.payload = usage_data
+ end
+ end
+
def store_metrics(response)
metrics = response['conv_index'] || response['dev_ops_score']
@@ -44,4 +59,13 @@ class SubmitUsagePingService
metrics.slice(*METRICS)
)
end
+
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/233615 for details
+ def url
+ if Rails.env.production?
+ PRODUCTION_URL
+ else
+ STAGING_URL
+ end
+ end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index db5693960b2..6702596f17c 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -297,7 +297,7 @@ module SystemNoteService
end
def new_alert_issue(alert, issue, author)
- ::SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project, author: author).new_alert_issue(alert, issue)
+ ::SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project, author: author).new_alert_issue(issue)
end
private
diff --git a/app/services/system_notes/alert_management_service.rb b/app/services/system_notes/alert_management_service.rb
index 55a6a17bbca..f835376727a 100644
--- a/app/services/system_notes/alert_management_service.rb
+++ b/app/services/system_notes/alert_management_service.rb
@@ -12,7 +12,7 @@ module SystemNotes
#
# Returns the created Note object
def change_alert_status(alert)
- status = AlertManagement::Alert::STATUSES.key(alert.status).to_s.titleize
+ status = alert.state.to_s.titleize
body = "changed the status to **#{status}**"
create_note(NoteSummary.new(noteable, project, author, body, action: 'status'))
@@ -20,7 +20,6 @@ module SystemNotes
# Called when an issue is created based on an AlertManagement::Alert
#
- # alert - AlertManagement::Alert object.
# issue - Issue object.
#
# Example Note text:
@@ -28,10 +27,25 @@ module SystemNotes
# "created issue #17 for this alert"
#
# Returns the created Note object
- def new_alert_issue(alert, issue)
+ def new_alert_issue(issue)
body = "created issue #{issue.to_reference(project)} for this alert"
create_note(NoteSummary.new(noteable, project, author, body, action: 'alert_issue_added'))
end
+
+ # Called when an AlertManagement::Alert is resolved due to the associated issue being closed
+ #
+ # issue - Issue object.
+ #
+ # Example Note text:
+ #
+ # "changed the status to Resolved by closing issue #17"
+ #
+ # Returns the created Note object
+ def closed_alert_issue(issue)
+ body = "changed the status to **Resolved** by closing issue #{issue.to_reference(project)}"
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'status'))
+ end
end
end
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 76261aa716e..7535db54130 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -341,7 +341,7 @@ module SystemNotes
def state_change_tracking_enabled?
noteable.respond_to?(:resource_state_events) &&
- ::Feature.enabled?(:track_resource_state_change_events, noteable.project)
+ ::Feature.enabled?(:track_resource_state_change_events, noteable.project, default_enabled: true)
end
end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index ec15bdde8d7..a3db2ae7947 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -49,11 +49,11 @@ class TodoService
todo_users.each(&:update_todos_count_cache)
end
- # When we reassign an issuable we should:
+ # When we reassign an assignable object (issuable, alert) we should:
#
- # * create a pending todo for new assignee if issuable is assigned
+ # * create a pending todo for new assignee if object is assigned
#
- def reassigned_issuable(issuable, current_user, old_assignees = [])
+ def reassigned_assignable(issuable, current_user, old_assignees = [])
create_assignment_todo(issuable, current_user, old_assignees)
end
@@ -154,14 +154,6 @@ class TodoService
resolve_todos_for_target(awardable, current_user)
end
- # When assigning an alert we should:
- #
- # * create a pending todo for new assignee if alert is assigned
- #
- def assign_alert(alert, current_user)
- create_assignment_todo(alert, current_user, [])
- end
-
# When user marks a target as todo
def mark_todo(target, current_user)
attributes = attributes_for_todo(target.project, target, current_user, Todo::MARKED)
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index 621266f00e1..d0939d5a542 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -53,7 +53,13 @@ module Users
current = current_authorizations_per_project
fresh = fresh_access_levels_per_project
- remove = current.each_with_object([]) do |(project_id, row), array|
+ # Delete projects that have more than one authorizations associated with
+ # the user. The correct authorization is added to the ``add`` array in the
+ # next stage.
+ remove = projects_with_duplicates
+ current.except!(*projects_with_duplicates)
+
+ remove |= current.each_with_object([]) do |(project_id, row), array|
# rows not in the new list or with a different access level should be
# removed.
if !fresh[project_id] || fresh[project_id] != row.access_level
@@ -106,7 +112,7 @@ module Users
end
def current_authorizations
- user.project_authorizations.select(:project_id, :access_level)
+ @current_authorizations ||= user.project_authorizations.select(:project_id, :access_level)
end
def fresh_authorizations
@@ -116,5 +122,12 @@ module Users
private
attr_reader :incorrect_auth_found_callback, :missing_auth_found_callback
+
+ def projects_with_duplicates
+ @projects_with_duplicates ||= current_authorizations
+ .group_by(&:project_id)
+ .select { |project_id, authorizations| authorizations.count > 1 }
+ .keys
+ end
end
end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 91a26ff45b1..d6cb0729d6f 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -13,6 +13,7 @@ class WebHookService
end
end
+ REQUEST_BODY_SIZE_LIMIT = 25.megabytes
GITLAB_EVENT_HEADER = 'X-Gitlab-Event'
attr_accessor :hook, :data, :hook_name, :request_options
@@ -53,17 +54,18 @@ class WebHookService
http_status: response.code,
message: response.to_s
}
- rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep => e
+ rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError, Gitlab::HTTP::RedirectionTooDeep, Gitlab::Json::LimitedEncoder::LimitExceeded => e
+ execution_duration = Gitlab::Metrics::System.monotonic_time - start_time
log_execution(
trigger: hook_name,
url: hook.url,
request_data: data,
response: InternalErrorResponse.new,
- execution_duration: Gitlab::Metrics::System.monotonic_time - start_time,
+ execution_duration: execution_duration,
error_message: e.to_s
)
- Gitlab::AppLogger.error("WebHook Error => #{e}")
+ Gitlab::AppLogger.error("WebHook Error after #{execution_duration.to_i.seconds}s => #{e}")
{
status: :error,
@@ -83,7 +85,7 @@ class WebHookService
def make_request(url, basic_auth = false)
Gitlab::HTTP.post(url,
- body: data.to_json,
+ body: Gitlab::Json::LimitedEncoder.encode(data, limit: REQUEST_BODY_SIZE_LIMIT),
headers: build_headers(hook_name),
verify: hook.enable_ssl_verification,
basic_auth: basic_auth,
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
index 2967684f7bc..fd234630633 100644
--- a/app/services/wiki_pages/base_service.rb
+++ b/app/services/wiki_pages/base_service.rb
@@ -44,7 +44,9 @@ module WikiPages
end
def create_wiki_event(page)
- response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
+ response = WikiPages::EventCreateService
+ .new(current_user)
+ .execute(slug_for_page(page), page, event_action, fingerprint(page))
log_error(response.message) if response.error?
end
@@ -52,6 +54,10 @@ module WikiPages
def slug_for_page(page)
page.slug
end
+
+ def fingerprint(page)
+ page.sha
+ end
end
end
diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb
index 63107445782..9702876effa 100644
--- a/app/services/wiki_pages/create_service.rb
+++ b/app/services/wiki_pages/create_service.rb
@@ -10,7 +10,11 @@ module WikiPages
execute_hooks(page)
end
- page
+ if page.persisted?
+ ServiceResponse.success(payload: { page: page })
+ else
+ ServiceResponse.error(message: _('Could not create wiki page'), payload: { page: page })
+ end
end
def usage_counter_action
diff --git a/app/services/wiki_pages/destroy_service.rb b/app/services/wiki_pages/destroy_service.rb
index d59c27bb92a..ab5abe1c82b 100644
--- a/app/services/wiki_pages/destroy_service.rb
+++ b/app/services/wiki_pages/destroy_service.rb
@@ -21,5 +21,9 @@ module WikiPages
def event_action
:destroyed
end
+
+ def fingerprint(page)
+ page.wiki.repository.head_commit.sha
+ end
end
end
diff --git a/app/services/wiki_pages/event_create_service.rb b/app/services/wiki_pages/event_create_service.rb
index 0453c90d693..ebfc2414f9e 100644
--- a/app/services/wiki_pages/event_create_service.rb
+++ b/app/services/wiki_pages/event_create_service.rb
@@ -9,11 +9,11 @@ module WikiPages
@author = author
end
- def execute(slug, page, action)
+ def execute(slug, page, action, event_fingerprint)
event = Event.transaction do
wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
- ::EventCreateService.new.wiki_event(wiki_page_meta, author, action)
+ ::EventCreateService.new.wiki_event(wiki_page_meta, author, action, event_fingerprint)
end
ServiceResponse.success(payload: { event: event })
diff --git a/app/uploaders/ci/pipeline_artifact_uploader.rb b/app/uploaders/ci/pipeline_artifact_uploader.rb
new file mode 100644
index 00000000000..d3a83c5d633
--- /dev/null
+++ b/app/uploaders/ci/pipeline_artifact_uploader.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Ci
+ class PipelineArtifactUploader < GitlabUploader
+ include ObjectStorage::Concern
+
+ storage_options Gitlab.config.artifacts
+
+ alias_method :upload, :model
+
+ def store_dir
+ dynamic_segment
+ end
+
+ private
+
+ def dynamic_segment
+ Gitlab::HashedPath.new('pipelines', model.pipeline_id, 'artifacts', model.id, root_hash: model.project_id)
+ end
+ end
+end
diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb
index 400f0b3dcc6..47976c909e8 100644
--- a/app/uploaders/job_artifact_uploader.rb
+++ b/app/uploaders/job_artifact_uploader.rb
@@ -36,15 +36,10 @@ class JobArtifactUploader < GitlabUploader
end
def hashed_path
- File.join(disk_hash[0..1], disk_hash[2..3], disk_hash,
- model.created_at.utc.strftime('%Y_%m_%d'), model.job_id.to_s, model.id.to_s)
+ Gitlab::HashedPath.new(model.created_at.utc.strftime('%Y_%m_%d'), model.job_id, model.id, root_hash: model.project_id)
end
def legacy_path
File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.job_id.to_s)
end
-
- def disk_hash
- @disk_hash ||= Digest::SHA2.hexdigest(model.project_id.to_s)
- end
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index 63b6197a04d..ac1f022c63f 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -169,10 +169,6 @@ module ObjectStorage
object_store_options.connection.to_hash.deep_symbolize_keys
end
- def consolidated_settings?
- object_store_options.fetch('consolidated_settings', false)
- end
-
def remote_store_path
object_store_options.remote_directory
end
@@ -193,14 +189,18 @@ module ObjectStorage
File.join(self.root, TMP_UPLOAD_PATH)
end
+ def object_store_config
+ ObjectStorage::Config.new(object_store_options)
+ end
+
def workhorse_remote_upload_options(has_length:, maximum_size: nil)
return unless self.object_store_enabled?
return unless self.direct_upload_enabled?
id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-')
upload_path = File.join(TMP_UPLOAD_PATH, id)
- direct_upload = ObjectStorage::DirectUpload.new(self.object_store_credentials, remote_store_path, upload_path,
- has_length: has_length, maximum_size: maximum_size, consolidated_settings: consolidated_settings?)
+ direct_upload = ObjectStorage::DirectUpload.new(self.object_store_config, upload_path,
+ has_length: has_length, maximum_size: maximum_size)
direct_upload.to_hash.merge(ID: id)
end
@@ -283,6 +283,10 @@ module ObjectStorage
self.class.object_store_credentials
end
+ def fog_attributes
+ @fog_attributes ||= self.class.object_store_config.fog_attributes
+ end
+
# Set ACL of uploaded objects to not-public (fog-aws)[1] or no ACL at all
# (fog-google). Value is ignored by other supported backends (fog-aliyun,
# fog-openstack, fog-rackspace)
diff --git a/app/uploaders/packages/package_file_uploader.rb b/app/uploaders/packages/package_file_uploader.rb
index 20fcf0a7a32..28545b9fcdf 100644
--- a/app/uploaders/packages/package_file_uploader.rb
+++ b/app/uploaders/packages/package_file_uploader.rb
@@ -20,11 +20,6 @@ class Packages::PackageFileUploader < GitlabUploader
private
def dynamic_segment
- File.join(disk_hash[0..1], disk_hash[2..3], disk_hash,
- 'packages', model.package.id.to_s, 'files', model.id.to_s)
- end
-
- def disk_hash
- @disk_hash ||= Digest::SHA2.hexdigest(model.package.project_id.to_s)
+ Gitlab::HashedPath.new('packages', model.package.id, 'files', model.id, root_hash: model.package.project_id)
end
end
diff --git a/app/validators/html_safety_validator.rb b/app/validators/html_safety_validator.rb
index 29e7d445697..6ba009fa534 100644
--- a/app/validators/html_safety_validator.rb
+++ b/app/validators/html_safety_validator.rb
@@ -21,7 +21,7 @@ class HtmlSafetyValidator < ActiveModel::EachValidator
end
def self.error_message
- _("cannot contain HTML/XML tags, including any word between angle brackets (<,>).")
+ _("cannot contain HTML/XML tags, including any word between angle brackets (&lt;,&gt;).")
end
private
diff --git a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
index 1154a4c45b8..995f2ad6616 100644
--- a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
+++ b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
@@ -4,83 +4,48 @@
"field" : "SECURE_ANALYZERS_PREFIX",
"label" : "Image prefix",
"type": "string",
- "default_value": "registry.gitlab.com/gitlab-org/security-products/analyzers",
- "value": ""
+ "default_value": "",
+ "value": "",
+ "size": "MEDIUM",
+ "description": "Analyzer image's registry prefix (or Name of the registry providing the analyzers' image)"
},
{
"field" : "SAST_EXCLUDED_PATHS",
"label" : "Excluded Paths",
"type": "string",
- "default_value": "spec, test, tests, tmp",
- "value": ""
+ "default_value": "",
+ "value": "",
+ "size": "LARGE",
+ "description": "Comma-separated list of paths to be excluded from analyzer output. Patterns can be globs, file paths, or folder paths."
},
{
- "field" : "SECURE_ANALYZER_IMAGE_TAG",
+ "field" : "SAST_ANALYZER_IMAGE_TAG",
"label" : "Image tag",
"type": "string",
- "options": [],
- "default_value": "2",
- "value": ""
- },
- {
- "field" : "SAST_DISABLED",
- "label" : "Disable SAST",
- "type": "options",
- "options": [
- {
- "value" :"true",
- "label" : "true (disables SAST)"
- },
- {
- "value":"false",
- "label":"false (enables SAST)"
- }
- ],
- "default_value": "false",
- "value": ""
+ "default_value": "",
+ "value": "",
+ "size": "SMALL",
+ "description": "Analyzer image's tag"
}
],
"pipeline": [
{
"field" : "stage",
"label" : "Stage",
- "type": "dropdown",
- "options": [
- {
- "value" :"test",
- "label" : "test"
- },
- {
- "value":"build",
- "label":"build"
- }
- ],
- "default_value": "test",
- "value": ""
- },
- {
- "field" : "allow_failure",
- "label" : "Allow Failure",
- "type": "options",
- "options": [
- {
- "value" :"true",
- "label" : "Allows pipeline failure"
- },
- {
- "value": "false",
- "label": "Does not allow pipeline failure"
- }
- ],
- "default_value": "true",
- "value": ""
+ "type": "string",
+ "default_value": "",
+ "value": "",
+ "size": "MEDIUM",
+ "description": "Pipeline stage in which the scan jobs run"
},
{
- "field" : "rules",
- "label" : "Rules",
- "type": "multiline",
+ "field" : "SEARCH_MAX_DEPTH",
+ "label" : "Search maximum depth",
+ "type": "string",
"default_value": "",
- "value": ""
+ "value": "",
+ "size": "SMALL",
+ "description": "Maximum depth of language and framework detection"
}
],
"analyzers": [
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 65a2f1d42e1..184249bcaba 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -9,14 +9,6 @@
= _('Gravatar enabled')
.form-group
- = f.label :namespace_storage_size_limit, class: 'label-bold' do
- = _('Maximum namespace storage (MB)')
- = f.number_field :namespace_storage_size_limit, class: 'form-control', min: 0
- %span.form-text.text-muted
- = _('Includes repository storage, wiki storage, LFS objects, build artifacts and packages. 0 for unlimited.')
- = link_to _('More information'), help_page_path('user/admin_area/settings/account_and_limit_settings', anchor: 'maximum-namespace-storage-size'), target: '_blank'
-
- .form-group
= f.label :default_projects_limit, _('Default projects limit'), class: 'label-bold'
= f.number_field :default_projects_limit, class: 'form-control', title: _('Maximum number of projects.'), data: { toggle: 'tooltip', container: 'body' }
.form-group
@@ -52,7 +44,7 @@
= f.check_box :user_default_external, class: 'form-check-input'
= f.label :user_default_external, class: 'form-check-label' do
= _('Newly registered users will by default be external')
- .prepend-top-10
+ .gl-mt-3
= _('Internal users')
= f.text_field :user_default_internal_regex, placeholder: _('Regex pattern'), class: 'form-control gl-mt-2'
.help-block
@@ -68,5 +60,5 @@
= render_if_exists 'admin/application_settings/updating_name_disabled_for_users', form: f
= render_if_exists 'admin/application_settings/availability_on_namespace_setting', form: f
-
- = f.submit _('Save changes'), class: 'btn btn-success qa-save-changes-button'
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: 'btn btn-success qa-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 410820dfb85..7051b790fb7 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -39,13 +39,13 @@
= f.label :default_artifacts_expire_in, _('Default artifacts expiration'), class: 'label-bold'
= f.text_field :default_artifacts_expire_in, class: 'form-control'
.form-text.text-muted
- = _("Set the default expiration time for each job's artifacts. 0 for unlimited. The default unit is in seconds, but you can define an alternative. For example: <code>4 mins 2 sec</code>, <code>2h42min</code>.").html_safe
+ = html_escape(_("Set the default expiration time for each job's artifacts. 0 for unlimited. The default unit is in seconds, but you can define an alternative. For example: %{code_open}4 mins 2 sec%{code_close}, %{code_open}2h42min%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration-core-only')
.form-group
= f.label :archive_builds_in_human_readable, _('Archive jobs'), class: 'label-bold'
= f.text_field :archive_builds_in_human_readable, class: 'form-control', placeholder: 'never'
.form-text.text-muted
- = _("Set the duration for which the jobs will be considered as old and expired. Once that time passes, the jobs will be archived and no longer able to be retried. Make it empty to never expire jobs. It has to be no less than 1 day, for example: <code>15 days</code>, <code>1 month</code>, <code>2 years</code>.").html_safe
+ = html_escape(_("Set the duration for which the jobs will be considered as old and expired. Once that time passes, the jobs will be archived and no longer able to be retried. Make it empty to never expire jobs. It has to be no less than 1 day, for example: %{code_open}15 days%{code_close}, %{code_open}1 month%{code_close}, %{code_open}2 years%{code_close}.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
.form-group
.form-check
= f.check_box :protected_ci_variables, class: 'form-check-input'
diff --git a/app/views/admin/application_settings/_diff_limits.html.haml b/app/views/admin/application_settings/_diff_limits.html.haml
index 137b7281e0f..16b7fbe1ab6 100644
--- a/app/views/admin/application_settings/_diff_limits.html.haml
+++ b/app/views/admin/application_settings/_diff_limits.html.haml
@@ -12,5 +12,5 @@
= link_to icon('question-circle'),
help_page_path('user/admin_area/diff_limits',
anchor: 'maximum-diff-patch-size')
-
- = f.submit _('Save changes'), class: 'btn btn-success'
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/admin/application_settings/_eks.html.haml b/app/views/admin/application_settings/_eks.html.haml
index d959b4f9b43..d74afcd3e64 100644
--- a/app/views/admin/application_settings/_eks.html.haml
+++ b/app/views/admin/application_settings/_eks.html.haml
@@ -9,7 +9,7 @@
= _('Amazon EKS integration allows you to provision EKS clusters from GitLab.')
.settings-content
- = form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-eks-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-eks-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
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 73412133979..e82ed0db851 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,5 +47,5 @@
.form-group
= f.label :external_authorization_service_default_label, _('Default classification label'), class: 'label-bold'
= f.text_field :external_authorization_service_default_label, class: 'form-control'
-
- = f.submit 'Save changes', class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_initial_branch_name.html.haml b/app/views/admin/application_settings/_initial_branch_name.html.haml
index e76374e88a8..acbf971e4b9 100644
--- a/app/views/admin/application_settings/_initial_branch_name.html.haml
+++ b/app/views/admin/application_settings/_initial_branch_name.html.haml
@@ -9,4 +9,6 @@
= f.text_field :default_branch_name, placeholder: 'master', class: 'form-control'
%span.form-text.text-muted
= (_("Changes affect new repositories only. If not specified, Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name } ).html_safe
- = f.submit _('Save changes'), class: 'gl-button btn-success'
+
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: 'gl-button btn-success'
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index d35774d330d..f2011257b8c 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -8,7 +8,7 @@
%p
= _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
.settings-content
- = form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting) if expanded
%fieldset
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index 417916d8c25..6f9d3a889cd 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -14,9 +14,12 @@
%a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck'
in all project and wiki repositories to look for silent disk corruption issues.
.form-group
- = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
.form-text.text-muted
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
+ - clear_repository_checks_link = _('Clear all repository checks')
+ - clear_repository_checks_message = _('This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?')
+ .gl-display-flex.gl-justify-content-end
+ = link_to clear_repository_checks_link, clear_repository_check_states_admin_application_settings_path, data: { confirm: clear_repository_checks_message }, method: :put, class: "btn btn-sm btn-remove"
.sub-section
%h4 Housekeeping
@@ -53,4 +56,5 @@
.form-text.text-muted
Number of Git pushes after which 'git gc' is run.
- = f.submit 'Save changes', class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: "btn btn-success"
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 8ec9b3c528a..3b1d1eceb9c 100644
--- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -14,4 +14,5 @@
= render_if_exists 'admin/application_settings/mirror_settings', form: f
- = f.submit _('Save changes'), class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_static_objects.html.haml b/app/views/admin/application_settings/_repository_static_objects.html.haml
index 03aa48b2282..9bc751adc8b 100644
--- a/app/views/admin/application_settings/_repository_static_objects.html.haml
+++ b/app/views/admin/application_settings/_repository_static_objects.html.haml
@@ -15,4 +15,5 @@
%span.form-text.text-muted#static_objects_external_storage_auth_token_help_block
= _('A secure token that identifies an external storage request.')
- = f.submit _('Save changes'), class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index ecae720cd49..ee55529621b 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -22,5 +22,5 @@
= f.text_field attribute[:name], class: 'form-text-input', value: attribute[:value]
= f.label attribute[:label], attribute[:label], class: 'label-bold form-check-label'
%br
-
- = f.submit _('Save changes'), class: "btn btn-success qa-save-changes-button"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: "btn btn-success qa-save-changes-button"
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 0972e10e12c..505be869620 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -57,5 +57,5 @@
= f.label :sign_in_text, class: 'label-bold'
= f.text_area :sign_in_text, class: 'form-control', rows: 4
.form-text.text-muted Markdown enabled
-
- = f.submit 'Save changes', class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml
index d8495c82af1..3b88696dc51 100644
--- a/app/views/admin/application_settings/_signup.html.haml
+++ b/app/views/admin/application_settings/_signup.html.haml
@@ -67,5 +67,5 @@
= f.label :after_sign_up_text, class: 'label-bold'
= f.text_area :after_sign_up_text, class: 'form-control', rows: 4
.form-text.text-muted Markdown enabled
-
- = f.submit 'Save changes', class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml
index a2597433270..3216d7b7a9a 100644
--- a/app/views/admin/application_settings/_snowplow.html.haml
+++ b/app/views/admin/application_settings/_snowplow.html.haml
@@ -8,8 +8,7 @@
%p
= _('Configure the %{link} integration.').html_safe % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank') }
.settings-content
-
- = form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting) if expanded
%fieldset
diff --git a/app/views/admin/application_settings/_sourcegraph.html.haml b/app/views/admin/application_settings/_sourcegraph.html.haml
index 23cda0334a2..7650526dfc0 100644
--- a/app/views/admin/application_settings/_sourcegraph.html.haml
+++ b/app/views/admin/application_settings/_sourcegraph.html.haml
@@ -16,7 +16,7 @@
.settings-content
- = form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-sourcegraph-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-sourcegraph-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
index 654aed54a15..25d23ea7a84 100644
--- a/app/views/admin/application_settings/_terminal.html.haml
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -8,5 +8,5 @@
.form-text.text-muted
Maximum time for web terminal websocket connection (in seconds).
0 for unlimited.
-
- = f.submit 'Save changes', class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
index 19e7ab7c99a..a6d03ac1dde 100644
--- a/app/views/admin/application_settings/_terms.html.haml
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -15,5 +15,5 @@
= f.text_area :terms, class: 'form-control', rows: 8
.form-text.text-muted
= _("Markdown enabled")
-
- = f.submit _("Save changes"), class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _("Save changes"), class: "btn btn-success"
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 256b1f74bfa..0ed7341986d 100644
--- a/app/views/admin/application_settings/_third_party_offers.html.haml
+++ b/app/views/admin/application_settings/_third_party_offers.html.haml
@@ -8,8 +8,7 @@
%p
= _('Control the display of third party offers.')
.settings-content
-
- = form_for @application_setting, url: integrations_admin_application_settings_path(anchor: 'js-third-party-offers-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-third-party-offers-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting) if expanded
%fieldset
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index 3c4fc75dbee..28208d923db 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -66,5 +66,5 @@
.form-group
= f.label field_name, "#{type.upcase} SSH keys", class: 'label-bold'
= f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
-
- = f.submit _('Save changes'), class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/ci/_header.html.haml b/app/views/admin/application_settings/ci/_header.html.haml
index fe86284ba2f..aa1a6256986 100644
--- a/app/views/admin/application_settings/ci/_header.html.haml
+++ b/app/views/admin/application_settings/ci/_header.html.haml
@@ -8,13 +8,13 @@
= expanded ? _('Collapse') : _('Expand')
%p
- = _('Environment variables are applied to all project environments in this instance via the Runner. You can use environment variables for passwords, secret keys, etc. Make variables available to the running application by prepending the variable key with <code>K8S_SECRET_</code>. You can set variables to be:').html_safe
+ = html_escape(_('Environment variables are applied to environments via the Runner. You can use environment variables for passwords, secret keys, etc. Make variables available to the running application by prepending the variable key with %{code_open}K8S_SECRET_%{code_close}. You can set variables to be:')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%ul
%li
- = _('<code>Protected</code> to expose them to protected branches or tags only.').html_safe
+ = html_escape(_('%{code_open}Protected%{code_close} variables are only exposed to protected branches or tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
- = _('<code>Masked</code> to prevent the values from being displayed in job logs (must match certain regexp requirements).').html_safe
+ = html_escape(_('%{code_open}Masked%{code_close} variables are hidden in job logs (though they must match certain regexp requirements to do so).')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%p
= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'instance-level-cicd-environment-variables')
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index fd3f04fefd1..788dc0b0f1b 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -13,7 +13,7 @@
.settings-content
= render 'visibility_and_access'
-%section.settings.qa-account-and-limit-settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?) }
+%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'account_and_limit_settings_content' } }
.settings-header
%h4
= _('Account and limit')
@@ -101,8 +101,8 @@
= s_('IDE|Live Preview')
%span.form-text.text-muted
= s_('IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview.')
-
- = f.submit _('Save changes'), class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: "btn btn-success"
- if Feature.enabled?(:maintenance_mode)
%section.settings.no-animate#js-maintenance-mode-toggle{ class: ('expanded' if expanded_by_default?) }
@@ -116,11 +116,10 @@
.settings-content
#js-maintenance-mode-settings
-- if Feature.enabled?(:instance_level_integrations)
- = render_if_exists 'admin/application_settings/elasticsearch_form'
- = render 'admin/application_settings/plantuml'
- = render 'admin/application_settings/sourcegraph'
- = render_if_exists 'admin/application_settings/slack'
- = render 'admin/application_settings/third_party_offers'
- = render 'admin/application_settings/snowplow'
- = render 'admin/application_settings/eks'
+= render_if_exists 'admin/application_settings/elasticsearch_form'
+= render 'admin/application_settings/plantuml'
+= render 'admin/application_settings/sourcegraph'
+= render_if_exists 'admin/application_settings/slack'
+= render 'admin/application_settings/third_party_offers'
+= render 'admin/application_settings/snowplow'
+= render 'admin/application_settings/eks'
diff --git a/app/views/admin/application_settings/integrations.html.haml b/app/views/admin/application_settings/integrations.html.haml
index cca0240462f..b5dae424b46 100644
--- a/app/views/admin/application_settings/integrations.html.haml
+++ b/app/views/admin/application_settings/integrations.html.haml
@@ -2,29 +2,19 @@
- page_title _('Integrations')
- @content_class = 'limit-container-width' unless fluid_layout
-- if Feature.enabled?(:instance_level_integrations)
- - if show_admin_integrations_moved?
- .gl-alert.gl-alert-info.js-admin-integrations-moved.mt-3{ role: 'alert', data: { feature_id: UserCalloutsHelper::ADMIN_INTEGRATIONS_MOVED, dismiss_endpoint: user_callouts_path } }
- = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
- .gl-alert-body
- %h4.gl-alert-title= s_('AdminSettings|Some settings have moved')
- = s_('AdminSettings|Elasticsearch, PlantUML, Slack application, Third party offers, Snowplow, Amazon EKS have moved to Settings > General.')
- .gl-alert-actions
- = link_to s_('AdminSettings|Go to General Settings'), general_admin_application_settings_path, class: 'btn gl-alert-action btn-info new-gl-button'
+- if show_admin_integrations_moved?
+ .gl-alert.gl-alert-info.js-admin-integrations-moved.mt-3{ role: 'alert', data: { feature_id: UserCalloutsHelper::ADMIN_INTEGRATIONS_MOVED, dismiss_endpoint: user_callouts_path } }
+ = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
+ = sprite_icon('close', css_class: 'gl-icon')
+ .gl-alert-body
+ %h4.gl-alert-title= s_('AdminSettings|Some settings have moved')
+ = html_escape_once(s_('AdminSettings|Elasticsearch, PlantUML, Slack application, Third party offers, Snowplow, Amazon EKS have moved to Settings &gt; General.')).html_safe
+ .gl-alert-actions
+ = link_to s_('AdminSettings|Go to General Settings'), general_admin_application_settings_path, class: 'btn gl-alert-action btn-info new-gl-button'
- %h4= s_('AdminSettings|Apply integration settings to all Projects')
- %p
- = s_('AdminSettings|Integrations configured here will automatically apply to all projects on this instance.')
- = link_to _('Learn more'), '#'
- = render 'shared/integrations/index', integrations: @integrations
-
-- else
- = render_if_exists 'admin/application_settings/elasticsearch_form'
- = render 'admin/application_settings/plantuml'
- = render 'admin/application_settings/sourcegraph'
- = render_if_exists 'admin/application_settings/slack'
- = render 'admin/application_settings/third_party_offers'
- = render 'admin/application_settings/snowplow'
- = render 'admin/application_settings/eks'
+%h4= s_('AdminSettings|Apply integration settings to all Projects')
+%p
+ = s_('AdminSettings|Integrations configured here will automatically apply to all projects on this instance.')
+ = link_to _('Learn more'), '#'
+= render 'shared/integrations/index', integrations: @integrations
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index befe10ea510..181c54c2716 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -24,7 +24,7 @@
.settings-content
= render 'grafana'
-%section.settings.qa-performance-bar-settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?) }
+%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'performance_bar_settings_content' } }
.settings-header
%h4
= _('Profiling - Performance bar')
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 15149e46f9c..40fa86d8ea3 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -13,7 +13,7 @@
.settings-content
= render 'performance'
-%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'ip_limits_section' } }
+%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'ip_limits_content' } }
.settings-header
%h4
= _('User and IP Rate Limits')
@@ -24,7 +24,7 @@
.settings-content
= render 'ip_limits'
-%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'outbound_requests_section' } }
+%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'outbound_requests_content' } }
.settings-header
%h4
= _('Outbound requests')
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
index 0ad76e56d0b..787760516ce 100644
--- a/app/views/admin/application_settings/preferences.html.haml
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -2,7 +2,7 @@
- page_title _("Preferences")
- @content_class = "limit-container-width" unless fluid_layout
-%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'email_section' } }
+%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'email_content' } }
.settings-header
%h4
= _('Email')
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index 33a6715d424..18e093f7b2c 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -25,7 +25,7 @@
.settings-content
= render partial: 'repository_mirrors_form'
-%section.settings.qa-repository-storage-settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded_by_default?) }
+%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'repository_storage_settings_content' } }
.settings-header
%h4
= _('Repository storage')
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 79d758cf10b..8a937bd66cf 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -1,5 +1,5 @@
.broadcast-message.broadcast-banner-message.alert-warning.js-broadcast-banner-message-preview.mt-2{ style: broadcast_message_style(@broadcast_message), class: ('hidden' unless @broadcast_message.banner? ) }
- = sprite_icon('bullhorn', size: 16, css_class:'vertical-align-text-top')
+ = sprite_icon('bullhorn', css_class:'vertical-align-text-top')
.js-broadcast-message-preview
- if @broadcast_message.message.present?
= render_broadcast_message(@broadcast_message)
@@ -7,7 +7,7 @@
Your message here
.d-flex.justify-content-center
.broadcast-message.broadcast-notification-message.preview.js-broadcast-notification-message-preview.mt-2{ class: ('hidden' unless @broadcast_message.notification? ) }
- = sprite_icon('bullhorn', size: 16, css_class:'vertical-align-text-top')
+ = sprite_icon('bullhorn', css_class:'vertical-align-text-top')
.js-broadcast-message-preview
- if @broadcast_message.message.present?
= render_broadcast_message(@broadcast_message)
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index bca74f71c5c..a14d342bc14 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -37,8 +37,8 @@
= message.target_path
%td
= message.broadcast_type.capitalize
- %td.gl-white-space-nowrap
- = link_to sprite_icon('pencil-square'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn'
- = link_to sprite_icon('remove'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-danger'
+ %td.gl-white-space-nowrap.gl-display-flex
+ = link_to sprite_icon('pencil-square', 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 ml-2'
= paginate @broadcast_messages, theme: 'gitlab'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 7c6c21bc509..271ab12037e 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -1,15 +1,15 @@
- breadcrumb_title _("Dashboard")
- page_title _("Dashboard")
-- if show_license_breakdown?
- = render_if_exists 'admin/licenses/breakdown', license: @license
-
- if @notices
- @notices.each do |notice|
.js-vue-alert{ 'v-cloak': true, data: { variant: notice[:type],
dismissible: true.to_s } }
= notice[:message].html_safe
+- if show_license_breakdown?
+ = render_if_exists 'admin/licenses/breakdown', license: @license
+
.admin-dashboard.gl-mt-3
.row
.col-sm-4
diff --git a/app/views/admin/dashboard/stats.html.haml b/app/views/admin/dashboard/stats.html.haml
index f7f2c717308..78707235cb5 100644
--- a/app/views/admin/dashboard/stats.html.haml
+++ b/app/views/admin/dashboard/stats.html.haml
@@ -2,7 +2,7 @@
%h3.my-4
= s_('AdminArea|Users statistics')
-%table.table.gl-text-gray-700
+%table.table.gl-text-gray-500
%tr
%td.p-3
= s_('AdminArea|Users without a Group and Project')
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 4e9cfc13af0..3409e2ffc8a 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -1,11 +1,8 @@
- page_title _('Deploy Keys')
-
-%h3.page-title.deploy-keys-title
- = _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.load.size }
- .float-right
- = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'btn btn-success btn-sm btn-inverted'
-
- if @deploy_keys.any?
+ %h3.page-title.deploy-keys-title
+ = _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.load.size }
+ = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'float-right btn btn-success btn-md gl-button'
.table-holder.deploy-keys-list
%table.table
%thead
@@ -32,3 +29,5 @@
.float-right
= link_to _('Edit'), edit_admin_deploy_key_path(deploy_key), class: 'btn btn-sm'
= link_to _('Remove'), admin_deploy_key_path(deploy_key), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn btn-sm btn-remove delete-key'
+- else
+ = render 'shared/empty_states/deploy_keys'
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index bbeeb1be929..ab817b2ef6e 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -14,7 +14,7 @@
.description
= markdown_field(group, :description)
- .stats.gl-text-gray-700.gl-flex-shrink-0.gl-display-none.gl-display-sm-flex
+ .stats.gl-text-gray-500.gl-flex-shrink-0.gl-display-none.gl-display-sm-flex
%span.badge.badge-pill
= storage_counter(group.storage_size)
@@ -22,15 +22,15 @@
= render_if_exists 'admin/groups/marked_for_deletion_badge', group: group, css_class: 'gl-ml-5'
%span.gl-ml-5
- = icon('bookmark')
+ = sprite_icon('bookmark', css_class: 'gl-vertical-align-text-bottom')
= number_with_delimiter(group.projects.count)
%span.gl-ml-5
- = icon('users')
+ = sprite_icon('users', css_class: 'gl-vertical-align-text-bottom')
= number_with_delimiter(group.users.count)
%span.gl-ml-5.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) }
- = visibility_level_icon(group.visibility_level, fw: false)
+ = visibility_level_icon(group.visibility_level)
.controls.gl-flex-shrink-0.gl-ml-5
= link_to _('Edit'), admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 4b0e0b9c697..6dd73698848 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -6,8 +6,8 @@
%h3.page-title
= _('Group: %{group_name}') % { group_name: @group.full_name }
- = link_to admin_group_edit_path(@group), class: "btn float-right", data: { qa_selector: 'edit_group_link' } do
- %i.fa.fa-pencil-square-o
+ = link_to admin_group_edit_path(@group), class: "btn btn-default gl-button float-right", data: { qa_selector: 'edit_group_link' } do
+ = sprite_icon('pencil-square', css_class: 'gl-icon')
= _('Edit')
%hr
.row
@@ -74,7 +74,7 @@
- @projects.each do |project|
%li
%strong
- = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
+ = link_to project.full_name, [:admin, project]
%span.badge.badge-pill
= storage_counter(project.statistics.storage_size)
%span.float-right.light
@@ -93,7 +93,7 @@
- shared_projects.each do |project|
%li
%strong
- = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
+ = link_to project.full_name, [:admin, project]
%span.badge.badge-pill
= storage_counter(project.statistics.storage_size)
%span.float-right.light
@@ -106,13 +106,13 @@
= _('Add user(s) to the group:')
.card-body.form-holder
%p.light
- - link_to_help = link_to(_("here"), help_page_path("user/permissions"))
- = _('Read more about project permissions <strong>%{link_to_help}</strong>').html_safe % { link_to_help: link_to_help }
+ - 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_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
- .prepend-top-10
+ .gl-mt-3
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
= button_tag _('Add users to group'), class: "btn btn-success"
@@ -120,10 +120,12 @@
.card
.card-header
- = _("<strong>%{group_name}</strong> group members").html_safe % { group_name: @group.name }
+ = html_escape(_("%{group_name} group members")) % { group_name: "<strong>#{html_escape(@group.name)}</strong>".html_safe }
%span.badge.badge-pill= @group.members.size
.float-right
- = link_to icon('pencil-square-o', text: _('Manage access')), group_group_members_path(@group), class: "btn btn-sm"
+ = link_to group_group_members_path(@group), class: 'btn btn-default gl-button btn-sm' do
+ = sprite_icon('pencil-square', css_class: 'gl-icon')
+ = _('Manage access')
%ul.content-list.group-users-list.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false }
.card-footer
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index 587bfba8d47..fbe37f6c509 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -7,7 +7,7 @@
%p
#{ s_('HealthCheck|Access token is') }
%code#health-check-token= Gitlab::CurrentSettings.health_check_access_token
- .prepend-top-10
+ .gl-mt-3
= button_to _("Reset health check access token"), reset_health_check_token_admin_application_settings_path,
method: :put, class: 'btn btn-default',
data: { confirm: _('Are you sure you want to reset the health check token?') }
diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml
index 4d534c59c40..a8ef19dcf46 100644
--- a/app/views/admin/hook_logs/show.html.haml
+++ b/app/views/admin/hook_logs/show.html.haml
@@ -1,9 +1,9 @@
- page_title _('Request details')
%h3.page-title
- Request details
+ = _("Request details")
%hr
-= link_to 'Resend Request', retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn btn-default float-right gl-ml-3"
+= link_to _("Resend Request"), retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn btn-default float-right gl-ml-3"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/admin/labels/destroy.js.haml b/app/views/admin/labels/destroy.js.haml
index 394d3c11f31..b9b63829f25 100644
--- a/app/views/admin/labels/destroy.js.haml
+++ b/app/views/admin/labels/destroy.js.haml
@@ -1,2 +1,2 @@
-- if @labels.size.zero?
+- if @labels.size == 0
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 96337d357eb..bd3b2f40059 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -6,8 +6,8 @@
.js-remove-member-modal
%h3.page-title
= _('Project: %{name}') % { name: @project.full_name }
- = link_to edit_project_path(@project), class: "btn btn-nr float-right" do
- %i.fa.fa-pencil-square-o
+ = link_to edit_project_path(@project), class: "btn btn-default gl-button float-right" do
+ = sprite_icon('pencil-square', css_class: 'gl-icon')
= _('Edit')
%hr
- if @project.last_repository_check_failed?
@@ -178,8 +178,9 @@
= _('group members')
%span.badge.badge-pill= @group_members.size
.float-right
- = link_to admin_group_path(@group), class: 'btn btn-sm' do
- = icon('pencil-square-o', text: _('Manage access'))
+ = link_to admin_group_path(@group), class: 'btn btn-default gl-button btn-sm' do
+ = sprite_icon('pencil-square', css_class: 'gl-icon')
+ = _('Manage access')
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false }
.card-footer
@@ -193,7 +194,9 @@
= _('project members')
%span.badge.badge-pill= @project.users.size
.float-right
- = link_to icon('pencil-square-o', text: _('Manage access')), project_project_members_path(@project), class: "btn btn-sm"
+ = link_to project_project_members_path(@project), class: 'btn btn-default gl-button btn-sm' do
+ = sprite_icon('pencil-square', css_class: 'gl-icon')
+ = _('Manage access')
%ul.content-list.project_members.members-list
= render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false }
.card-footer
diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml
index 6e1ac452d52..6c75dfe9733 100644
--- a/app/views/admin/requests_profiles/index.html.haml
+++ b/app/views/admin/requests_profiles/index.html.haml
@@ -11,7 +11,7 @@
- if @profiles.present?
.gl-mt-3
- @profiles.each do |path, profiles|
- .card.card-small
+ .card
.card-header
%code= path
%ul.content-list
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 5c834c2125f..0bbe73d6f7e 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -66,13 +66,13 @@
.btn-group.table-action-buttons
.btn-group
= link_to admin_runner_path(runner), class: 'btn btn-default has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do
- = icon('pencil')
+ = sprite_icon('pencil')
.btn-group
- if runner.active?
- = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-default has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
- = icon('pause')
+ = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-default btn-svg has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
+ = sprite_icon('pause')
- else
- = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
+ = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default btn-svg has-tooltip gl-px-3', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
= sprite_icon('play')
.btn-group
= link_to [:admin, runner], method: :delete, class: 'btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 0c2b9bab357..cecf3f137ed 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -28,7 +28,7 @@
%p You can't make this a shared Runner.
%hr
-.append-bottom-20
+.gl-mb-6
= render 'shared/runners/form', runner: @runner, runner_form_url: admin_runner_path(@runner), in_gitlab_com_admin_context: Gitlab.com?
.row
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
index ec343c38470..19a0b7466a2 100644
--- a/app/views/admin/services/index.html.haml
+++ b/app/views/admin/services/index.html.haml
@@ -18,7 +18,7 @@
= link_to edit_admin_application_settings_integration_path(service.to_param), class: 'gl-text-blue-300!' do
%strong.has-tooltip{ title: s_('AdminSettings|Moved to integrations'), data: { container: 'body' } }
= service.title
- %td.gl-cursor-default.gl-text-gray-600
+ %td.gl-cursor-default.gl-text-gray-400
= service.description
%td
- else
diff --git a/app/views/admin/sessions/_signin_box.html.haml b/app/views/admin/sessions/_signin_box.html.haml
index cb6c0a76e56..ab7eb8c79dc 100644
--- a/app/views/admin/sessions/_signin_box.html.haml
+++ b/app/views/admin/sessions/_signin_box.html.haml
@@ -7,7 +7,7 @@
= render_if_exists 'devise/sessions/new_kerberos_tab'
- ldap_servers.each_with_index do |server, i|
- .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain)) }
+ .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain)) }
.login-body
= render 'devise/sessions/new_ldap', server: server, hide_remember_me: true, submit_message: _('Enter Admin Mode')
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 3403e9e5abf..a60dbd51935 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -12,10 +12,10 @@
.float-right
- if impersonation_enabled? && @user != current_user && @user.can?(:log_in)
- = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info", data: { qa_selector: 'impersonate_user_link' }
- = link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
+ = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-info gl-button btn-grouped", data: { qa_selector: 'impersonate_user_link' }
+ = link_to edit_admin_user_path(@user), class: "btn btn-default gl-button btn-grouped" do
+ = sprite_icon('pencil-square', css_class: 'gl-icon')
+ = _('Edit')
%hr
%ul.nav-links.nav.nav-tabs
= nav_link(path: 'users#show') do
diff --git a/app/views/admin/users/_user_listing_note.html.haml b/app/views/admin/users/_user_listing_note.html.haml
index b6c9bc43339..e5c43259b79 100644
--- a/app/views/admin/users/_user_listing_note.html.haml
+++ b/app/views/admin/users/_user_listing_note.html.haml
@@ -1,3 +1,3 @@
- if user.note.present?
%span.has-tooltip.user-note{ title: user.note }
- = sprite_icon('document', size: 16, css_class: 'gl-vertical-align-middle')
+ = sprite_icon('document', css_class: 'gl-vertical-align-middle')
diff --git a/app/views/ci/deploy_freeze/_index.html.haml b/app/views/ci/deploy_freeze/_index.html.haml
new file mode 100644
index 00000000000..fa4b3d5684e
--- /dev/null
+++ b/app/views/ci/deploy_freeze/_index.html.haml
@@ -0,0 +1,2 @@
+#js-deploy-freeze-table{ data: { project_id: @project.id, timezone_data: timezone_data.to_json } }
+
diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml
index aca8aa5d341..6c2bd2a5d2f 100644
--- a/app/views/ci/runner/_how_to_setup_runner.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -1,5 +1,5 @@
- link = link_to _("Install GitLab Runner"), 'https://docs.gitlab.com/runner/install/', target: '_blank'
-.append-bottom-10
+.gl-mb-3
%h4= _("Set up a %{type} Runner manually") % { type: type }
%ol
@@ -13,7 +13,7 @@
= _("Use the following registration token during setup:")
%code#registration_token= registration_token
= clipboard_button(target: '#registration_token', title: _("Copy token"), class: "btn-transparent btn-clipboard")
- .prepend-top-10.append-bottom-10
+ .gl-mt-3.gl-mb-3
= button_to _("Reset runners registration token"), reset_token_url,
method: :put, class: 'btn btn-default',
data: { confirm: _("Are you sure you want to reset registration token?") }
diff --git a/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml b/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml
index 58d2ef5d5e6..42710039757 100644
--- a/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner_automatically.html.haml
@@ -1,4 +1,4 @@
-.append-bottom-10
+.gl-mb-3
%h4= _('Set up a %{type} Runner automatically') % { type: type }
%p
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index d9d646c77d9..69cb41b1713 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -16,5 +16,5 @@
%span.ci-build-text.text-truncate.mw-70p.gl-pl-1-deprecated-no-really-do-not-use-me= subject.name
- if status.has_action?
- = link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
+ = link_to status.action_path, class: "gl-button ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
= sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}")
diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
index 144d13565b2..bf695d871f8 100644
--- a/app/views/ci/variables/_content.html.haml
+++ b/app/views/ci/variables/_content.html.haml
@@ -1,3 +1,8 @@
-= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want.')
-= _('You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>.').html_safe
+= html_escape(_('Environment variables are applied to environments via the Runner. You can use environment variables for passwords, secret keys, etc. Make variables available to the running application by prepending the variable key with %{code_open}K8S_SECRET_%{code_close}. You can set variables to be:')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+%ul
+ %li
+ = html_escape(_('%{code_open}Protected%{code_close} variables are only exposed to protected branches or tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ %li
+ = html_escape(_('%{code_open}Masked%{code_close} variables are hidden in job logs (though they must match certain regexp requirements to do so).')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+
= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables')
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 6672a8e5ea0..4c6eeb17c07 100644
--- a/app/views/ci/variables/_url_query_variable_row.html.haml
+++ b/app/views/ci/variables/_url_query_variable_row.html.haml
@@ -24,5 +24,5 @@
name: value_input_name,
placeholder: s_('CiVariables|Input variable value') }
= value
- %button.js-row-remove-button.ci-variable-row-remove-button.table-section.section-5.border-top-0{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
- = icon('minus-circle')
+ %button.btn.btn-svg.btn-item-remove.js-row-remove-button.ci-variable-row-remove-button.table-section{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
+ = sprite_icon('close')
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index c69a3adb0e9..542a41c2f7d 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -60,5 +60,5 @@
value: is_masked,
data: { default: is_masked_default.to_s } }
= render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable
- %button.js-row-remove-button.ci-variable-row-remove-button.table-section.section-5.border-top-0{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
- = icon('minus-circle')
+ %button.btn.btn-svg.js-row-remove-button.ci-variable-row-remove-button.table-section{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
+ = sprite_icon('close')
diff --git a/app/views/clusters/clusters/_advanced_settings.html.haml b/app/views/clusters/clusters/_advanced_settings.html.haml
index d1681409a93..117bdbc06a1 100644
--- a/app/views/clusters/clusters/_advanced_settings.html.haml
+++ b/app/views/clusters/clusters/_advanced_settings.html.haml
@@ -11,7 +11,7 @@
= @cluster.provider_label
%p
- provider_link = link_to(@cluster.provider_label, @cluster.provider_management_url, target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{provider_link}').html_safe % { provider_link: provider_link }
+ = html_escape(s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{provider_link}')) % { provider_link: provider_link }
.sub-section.form-group
= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster, html: { class: 'cluster_management_form' } do |field|
@@ -22,9 +22,10 @@
= project_select_tag('cluster[management_project_id]', class: 'hidden-filter-value', toggle_class: 'js-project-search js-project-filter js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
placeholder: _('Select project'), idAttribute: 'id', data: { order_by: 'last_activity_at', idattribute: 'id', simple_filter: true, allow_clear: true, include_groups: false, include_projects_in_subgroups: true, group_id: group_id, user_id: user_id }, value: @cluster.management_project_id)
.text-muted
- = s_('ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes <code>cluster-admin</code> privileges.').html_safe
+ = html_escape(s_('ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes %{code_open}cluster-admin%{code_close} privileges.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to _('More information'), help_page_path('user/clusters/management_project.md'), target: '_blank'
- = field.submit _('Save changes'), class: 'btn btn-success'
+ .gl-display-flex.gl-justify-content-end
+ = field.submit _('Save changes'), class: 'btn btn-success'
- if @cluster.managed?
.sub-section.form-group
@@ -32,7 +33,8 @@
= s_('ClusterIntegration|Clear cluster cache')
%p
= s_("ClusterIntegration|Clear the local cache of namespace and service accounts. This is necessary if your integration has become out of sync. The cache is repopulated during the next CI job that requires namespace and service accounts.")
- = link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn btn-primary')
+ .gl-display-flex.gl-justify-content-end
+ = link_to(s_('ClusterIntegration|Clear cluster cache'), clusterable.clear_cluster_cache_path(@cluster), method: :delete, class: 'btn btn-primary')
.sub-section.form-group
%h4.text-danger
diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml
index e9ad0c6a4e0..3461831eda2 100644
--- a/app/views/clusters/clusters/_banner.html.haml
+++ b/app/views/clusters/clusters/_banner.html.haml
@@ -7,16 +7,16 @@
%span.gl-ml-2= s_('ClusterIntegration|Kubernetes cluster is being created...')
.hidden.row.js-cluster-api-unreachable.gl-alert.gl-alert-warning{ role: 'alert' }
- = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ = sprite_icon('warning', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
%button.js-close-banner.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ = sprite_icon('close', css_class: 'gl-icon')
.gl-alert-body
= s_('ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct.')
.hidden.js-cluster-authentication-failure.js-cluster-api-unreachable.gl-alert.gl-alert-warning{ role: 'alert' }
- = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ = sprite_icon('warning', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
%button.js-close-banner.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ = sprite_icon('close', css_class: 'gl-icon')
.gl-alert-body
= s_('ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid.')
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index 3869ca6591c..54f6fa91cf1 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -3,10 +3,9 @@
%button.close.js-close{ type: "button" } &times;
.gcp-signup-offer--content
.gcp-signup-offer--icon.gl-mr-3
- = sprite_icon("information", size: 16)
+ = sprite_icon("information")
.gcp-signup-offer--copy
%h4= s_('ClusterIntegration|Did you know?')
%p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
%a.btn.btn-default{ href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', rel: 'noopener noreferrer' }
= s_("ClusterIntegration|Apply for credit")
-
diff --git a/app/views/clusters/clusters/_gitlab_integration_form.html.haml b/app/views/clusters/clusters/_gitlab_integration_form.html.haml
index 160964b532a..87af74a398f 100644
--- a/app/views/clusters/clusters/_gitlab_integration_form.html.haml
+++ b/app/views/clusters/clusters/_gitlab_integration_form.html.haml
@@ -1,36 +1,3 @@
= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster, html: { class: 'js-cluster-integration-form' } do |field|
= form_errors(@cluster)
- .form-group
- .d-flex.align-items-center
- %h4.pr-2.m-0
- = s_('ClusterIntegration|GitLab Integration')
- %label.gl-mb-0.js-cluster-enable-toggle-area{ title: s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.'), data: { toggle: 'tooltip', container: 'body' } }
- = render "shared/buttons/project_feature_toggle", is_checked: @cluster.enabled?, label: s_("ClusterIntegration|Toggle Kubernetes cluster"), disabled: !can?(current_user, :update_cluster, @cluster), data: { qa_selector: 'integration_status_toggle' } do
- = field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
-
- .form-group
- %h5= s_('ClusterIntegration|Environment scope')
- = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
- - environment_scope_url = help_page_path('user/project/clusters/index', anchor: 'base-domain')
- - environment_scope_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: environment_scope_url }
- .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster. %{environment_scope_start}More information%{environment_scope_end}").html_safe % { environment_scope_start: environment_scope_start, environment_scope_end: '</a>'.html_safe }
-
- .form-group
- %h5= s_('ClusterIntegration|Base domain')
- = field.text_field :base_domain, class: 'col-md-6 form-control js-select-on-focus', data: { qa_selector: 'base_domain_field' }
- .form-text.text-muted
- - auto_devops_url = help_page_path('topics/autodevops/index')
- - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
- = s_('ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe }
- %span{ :class => ["js-ingress-domain-help-text", ("hide" unless @cluster.application_ingress_external_ip.present?)] }
- = s_('ClusterIntegration|Alternatively')
- %code{ :class => "js-ingress-domain-snippet" }
- = s_('ClusterIntegration|%{external_ip}.nip.io').html_safe % { external_ip: @cluster.application_ingress_external_ip }
- = s_('ClusterIntegration| can be used instead of a custom domain.')
- - custom_domain_url = help_page_path('user/clusters/applications.md', anchor: 'pointing-your-dns-at-the-external-endpoint')
- - custom_domain_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: custom_domain_url }
- = s_('ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}.').html_safe % { custom_domain_start: custom_domain_start, custom_domain_end: '</a>'.html_safe }
-
- - if can?(current_user, :update_cluster, @cluster)
- .form-group
- = field.submit _('Save changes'), class: 'btn btn-success', data: { qa_selector: 'save_changes_button' }
+ #js-cluster-integration-form{ data: js_cluster_form_data(@cluster, can?(current_user, :update_cluster, @cluster)) }
diff --git a/app/views/clusters/clusters/_provider_details_form.html.haml b/app/views/clusters/clusters/_provider_details_form.html.haml
index fcb5d4402d6..e211851b939 100644
--- a/app/views/clusters/clusters/_provider_details_form.html.haml
+++ b/app/views/clusters/clusters/_provider_details_form.html.haml
@@ -48,5 +48,5 @@
- if cluster.allow_user_defined_namespace?
= render('clusters/clusters/namespace', platform_field: platform_field)
- .form-group
+ .form-group.gl-display-flex.gl-justify-content-end
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index ffa99f06593..6ac852af2db 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -35,7 +35,8 @@
deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'),
cloud_run_help_path: help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'cloud-run-for-anthos'),
manage_prometheus_path: manage_prometheus_path,
- cluster_id: @cluster.id } }
+ cluster_id: @cluster.id,
+ cilium_help_path: help_page_path('user/clusters/applications.md', anchor: 'install-cilium-using-gitlab-cicd')} }
.js-cluster-application-notice
.flash-container
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index 2db3e35250f..d617ee0e4cc 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,8 +1,8 @@
.nav-block.activities
= render 'shared/event_filter'
.controls
- = link_to dashboard_projects_path(rss_url_options), class: 'btn d-none d-sm-inline-block has-tooltip', title: 'Subscribe' do
- %i.fa.fa-rss
+ = link_to dashboard_projects_path(rss_url_options), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip', title: 'Subscribe' do
+ = sprite_icon('rss', css_class: 'qa-rss-icon gl-icon')
.content_list
.loading
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 5e78749fee2..fe91f9859f9 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -26,6 +26,7 @@
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, data: {placement: 'right'} do
= _("Explore projects")
+ = render_if_exists "dashboard/removed_projects_tab", removed_projects_count: @removed_projects_count
- unless feature_project_list_filter_bar
.nav-controls
= render 'shared/projects/search_form'
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 2e7eab87af3..54a5624c6dd 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -3,6 +3,14 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
+- if show_customize_homepage_banner?(@customize_homepage)
+ = content_for :customize_homepage_banner do
+ .d-none.d-md-block{ class: "gl-pt-6! gl-pb-2! #{(container_class unless @no_container)} #{@content_class}" }
+ .js-customize-homepage-banner{ data: { svg_path: image_path('illustrations/monitoring/getting_started.svg'),
+ preferences_behavior_path: profile_preferences_path(anchor: 'behavior'),
+ callouts_path: user_callouts_path,
+ callouts_feature_id: UserCalloutsHelper::CUSTOMIZE_HOMEPAGE } }
+
= render_dashboard_gold_trial(current_user)
- page_title _("Projects")
diff --git a/app/views/dashboard/projects/shared/_common.html.haml b/app/views/dashboard/projects/shared/_common.html.haml
new file mode 100644
index 00000000000..aa55f5a4e9c
--- /dev/null
+++ b/app/views/dashboard/projects/shared/_common.html.haml
@@ -0,0 +1,13 @@
+- @hide_top_links = true
+- breadcrumb_title _("Projects")
+- header_title _("Projects"), dashboard_projects_path
+
+= render_dashboard_gold_trial(current_user)
+
+= render "projects/last_push"
+= render 'dashboard/projects_head', project_tab_filter: :starred
+
+- if params[:filter_projects] || any_projects?(@projects)
+ = render 'projects'
+- else
+ = render empty_page
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 2924918aa4f..f1e8f262eed 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -1,14 +1 @@
-- @hide_top_links = true
-- breadcrumb_title _("Projects")
-- page_title _("Starred Projects")
-- header_title _("Projects"), dashboard_projects_path
-
-= render_dashboard_gold_trial(current_user)
-
-= render "projects/last_push"
-= render 'dashboard/projects_head', project_tab_filter: :starred
-
-- if params[:filter_projects] || any_projects?(@projects)
- = render 'projects'
-- else
- = render 'starred_empty_state'
+= render partial: 'dashboard/projects/shared/common', locals: {page_title: _('Starred Projects'), empty_page: 'starred_empty_state'}
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
index fb00e1b4384..afdf3c38567 100644
--- a/app/views/devise/registrations/new.html.haml
+++ b/app/views/devise/registrations/new.html.haml
@@ -3,7 +3,7 @@
.row
.col-lg-7
%h1.mb-3.font-weight-bold.text-6.mt-0
- = _("Speed up your DevOps<br>with GitLab").html_safe
+ = html_escape(_("Speed up your DevOps%{br_tag}with GitLab")) % { br_tag: '<br/>'.html_safe }
%p.text-3
= _("GitLab is a single application for the entire software development lifecycle. From project planning and source code management to CI/CD, monitoring, and security.")
.col-lg-5.order-12
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 6e9efcb0597..8c0ca6d4345 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,7 +1,7 @@
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
.form-group
= f.label _('Username or email'), for: 'user_login', class: 'label-bold'
- = f.text_field :login, class: 'form-control top', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field' }
+ = f.text_field :login, value: @invite_email, class: 'form-control top', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field' }
.form-group
= f.label :password, class: 'label-bold'
= f.password_field :password, class: 'form-control bottom', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
diff --git a/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml b/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml
index 61271f4525c..5f7345f306d 100644
--- a/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml
+++ b/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml
@@ -28,7 +28,7 @@
= f.label :password, class: 'label-bold'
= f.password_field :password, class: "form-control bottom", data: { qa_selector: 'new_user_password_field' }, required: true, pattern: ".{#{@minimum_password_length},}", title: _("Minimum length is %{minimum_password_length} characters.") % { minimum_password_length: @minimum_password_length }
%p.gl-field-hint.text-secondary= _('Minimum length is %{minimum_password_length} characters') % { minimum_password_length: @minimum_password_length }
- - if Gitlab::CurrentSettings.current_application_settings.enforce_terms?
+ - if Gitlab::CurrentSettings.current_application_settings.enforce_terms? && !experiment_enabled?(:terms_opt_in)
.form-group
= check_box_tag :terms_opt_in, '1', false, required: true, data: { qa_selector: 'new_user_accept_terms_checkbox' }
= label_tag :terms_opt_in do
@@ -41,5 +41,8 @@
= recaptcha_tags
.submit-container.mt-3
= f.submit _("Register"), class: "btn-register btn btn-block btn-success mb-0 p-2", data: { qa_selector: 'new_user_register_button' }
+ - if experiment_enabled?(:terms_opt_in)
+ %p.gl-text-gray-500.gl-mt-5.gl-mb-0
+ = html_escape(_("By clicking Register, I agree that I have read and accepted the GitLab %{linkStart}Terms of Use and Privacy Policy%{linkEnd}")) % { linkStart: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, linkEnd: '</a>'.html_safe }
- if omniauth_enabled? && button_based_providers_enabled?
= render 'devise/shared/experimental_separate_sign_up_flow_omniauth_box'
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index e99d0ac1105..6cf48f89876 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,6 +1,6 @@
- hide_remember_me = local_assigns.fetch(:hide_remember_me, false)
-.omniauth-container.prepend-top-15
+.omniauth-container.gl-mt-5
%label.label-bold.d-block
Sign in with
- providers = enabled_button_based_providers
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index c0b005bac77..d217b47527a 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -7,7 +7,7 @@
= render_if_exists 'devise/sessions/new_kerberos_tab'
- ldap_servers.each_with_index do |server, i|
- .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain)) }
+ .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain)) }
.login-body
= render 'devise/sessions/new_ldap', server: server
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 0735702ae5f..0da51d460e3 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -19,7 +19,7 @@
%p.validation-pending.gl-field-error-ignore.field-validation.hide= _('Checking username availability...')
.form-group
= f.label :email, class: 'label-bold'
- = f.email_field :email, class: "form-control middle", data: { qa_selector: 'new_user_email_field' }, required: true, title: _("Please provide a valid email address.")
+ = f.email_field :email, value: @invite_email, class: "form-control middle", data: { qa_selector: 'new_user_email_field' }, required: true, title: _("Please provide a valid email address.")
.form-group
= f.label :email_confirmation, class: 'label-bold'
= f.email_field :email_confirmation, class: "form-control middle", data: { qa_selector: 'new_user_email_confirmation_field' }, required: true, title: _("Please retype the email address.")
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index eb14ad6006f..acd41fb011a 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -8,7 +8,7 @@
= render_if_exists "devise/shared/kerberos_tab"
- ldap_servers.each_with_index do |server, i|
%li.nav-item
- = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain))}", data: { toggle: 'tab', qa_selector: 'ldap_tab' }
+ = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain))}", data: { toggle: 'tab', qa_selector: 'ldap_tab' }
= render_if_exists 'devise/shared/tab_smartcard'
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index d74cba984e8..7fbaa35d1d5 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -13,7 +13,7 @@
= _('Use one line per URI')
- if Doorkeeper.configuration.native_redirect_uri
%span.form-text.text-muted
- = _('Use <code>%{native_redirect_uri}</code> for local tests').html_safe % { native_redirect_uri: Doorkeeper.configuration.native_redirect_uri }
+ = html_escape(_('Use %{native_redirect_uri} for local tests')) % { native_redirect_uri: tag.code(Doorkeeper.configuration.native_redirect_uri) }
.form-group.form-check
= f.check_box :confidential, class: 'form-check-input'
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index 051799ca13f..6f781a635ba 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -44,7 +44,7 @@
= link_to edit_oauth_application_path(application), class: "btn btn-transparent gl-mr-2" do
%span.sr-only
= _('Edit')
- = icon('pencil')
+ = sprite_icon('pencil')
= render 'delete_form', application: application, small: true
- else
.settings-message.text-center
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 70abc1a267a..62e66486a3e 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -11,7 +11,7 @@
.text-warning
%p
= icon("exclamation-triangle fw")
- = _('You are an admin, which means granting access to <strong>%{client_name}</strong> will allow them to interact with GitLab as an admin as well. Proceed with caution.').html_safe % { client_name: @pre_auth.client.name }
+ = html_escape(_('You are an admin, which means granting access to %{client_name} will allow them to interact with GitLab as an admin as well. Proceed with caution.')) % { client_name: tag.strong(@pre_auth.client.name) }
%p
- link_to_client = link_to(@pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer')
= _("An application called %{link_to_client} is requesting access to your GitLab account.").html_safe % { link_to_client: link_to_client }
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index fd86d07fc86..f36f30d3638 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -12,7 +12,7 @@
- if cookies[:explore_groups_landing_dismissed] != 'true'
.explore-groups.landing.content-block.js-explore-groups-landing.hide
- %button.dismiss-button{ type: 'button', 'aria-label' => _('Dismiss') }= sprite_icon('close', size: 16)
+ %button.dismiss-button{ type: 'button', 'aria-label' => _('Dismiss') }= sprite_icon('close')
.svg-container
= custom_icon('icon_explore_groups_splash')
.inner-content
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index d00a3d266d8..6fc156cf4ed 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -5,8 +5,7 @@
.dropdown.js-project-filter-dropdown-wrap{ class: ('d-flex flex-grow-1 flex-shrink-1' if feature_project_list_filter_bar) }
%button.dropdown-menu-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
- unless has_label
- = icon('globe', class: 'mt-1')
- %span.light.ml-3= _("Visibility:")
+ %span= _("Visibility:")
- if params[:visibility_level].present?
= visibility_level_label(params[:visibility_level].to_i)
- else
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index 47e7e27de48..769455dc951 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -1,10 +1,9 @@
.nav-block.activities
= render 'shared/event_filter', show_group_events: @group.supports_events?
.controls
- = link_to group_path(@group, rss_url_options), class: 'btn d-none d-sm-inline-block has-tooltip' , title: 'Subscribe' do
- %i.fa.fa-rss
+ = link_to group_path(@group, rss_url_options), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip' , title: 'Subscribe' do
+ = sprite_icon('rss', css_class: 'qa-rss-icon gl-icon')
.content_list
.loading
.spinner.spinner-md
-
diff --git a/app/views/groups/_flash_messages.html.haml b/app/views/groups/_flash_messages.html.haml
index d1fea0e60c6..fa1a9d2cca4 100644
--- a/app/views/groups/_flash_messages.html.haml
+++ b/app/views/groups/_flash_messages.html.haml
@@ -1,3 +1,2 @@
= content_for :flash_message do
= render_if_exists 'shared/shared_runners_minutes_limit', namespace: @group, classes: [container_class, ("limit-container-width" unless fluid_layout)]
- = render_if_exists 'shared/namespace_storage_limit_alert', namespace: @group, classes: [container_class, ("limit-container-width" unless fluid_layout)]
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 2cf94695482..97e48cdec8c 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -12,7 +12,7 @@
%h1.home-panel-title.gl-mt-3.gl-mb-2
= @group.name
%span.visibility-icon.text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
- = visibility_level_icon(@group.visibility_level, fw: false, options: {class: 'icon'})
+ = visibility_level_icon(@group.visibility_level, options: {class: 'icon'})
.home-panel-metadata.d-flex.align-items-center.text-secondary
%span
= _("Group ID: %{group_id}") % { group_id: @group.id }
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 1e04b2761f6..eafee325500 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -15,7 +15,7 @@
.settings-content
= render 'groups/settings/general'
-%section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded), data: { qa_selector: 'permission_lfs_2fa_section' } }
+%section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded), data: { qa_selector: 'permission_lfs_2fa_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
= _('Permissions, LFS, 2FA')
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index b9ea8316bbc..c8e58a50b18 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,84 +1,99 @@
-- page_title _("Group members")
+- page_title _('Group members')
- can_manage_members = can?(current_user, :admin_group_member, @group)
- show_invited_members = can_manage_members && @invited_members.exists?
-- pending_active = params[:search_invited].present?
-- total_count = @members.count + @group.shared_with_group_links.count
+- show_access_requests = can_manage_members && @requesters.exists?
+- invited_active = params[:search_invited].present? || params[:invited_members_page].present?
+
+- form_item_label_css_class = 'label-bold gl-mr-2 gl-mb-0 gl-py-2 align-self-md-center'
.js-remove-member-modal
.project-members-page.gl-mt-3
%h4
- = _("Group members")
+ = _('Group members')
%hr
- if can_manage_members
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
- %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member")
+ %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _('Invite member')
%li.nav-tab{ role: 'presentation' }
- %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group")
+ %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _('Invite group')
.tab-content.gitlab-tab-content
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
= render_invite_member_for_group(@group, @group_member.access_level)
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
= render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access'
- = render 'shared/members/requests', membership_source: @group, requesters: @requesters
-
= render_if_exists 'groups/group_members/ldap_sync'
- %ul.nav-links.mobile-separator.nav.nav-tabs.clearfix
+ %ul.nav-links.mobile-separator.nav.nav-tabs
%li.nav-item
- = link_to "#existing_shares", class: ["nav-link", ("active" unless pending_active)] , 'data-toggle' => 'tab' do
+ = link_to '#tab-members', class: ['nav-link', ('active' unless invited_active)], data: { toggle: 'tab' } do
%span
- = _("Existing shares")
- %span.badge.badge-pill= total_count
+ = _('Members')
+ %span.badge.badge-pill= @members.total_count
+ - if @group.shared_with_group_links.any?
+ %li.nav-item
+ = link_to '#tab-groups', class: ['nav-link'] , data: { toggle: 'tab', qa_selector: 'groups_list_tab' } do
+ %span
+ = _('Groups')
+ %span.badge.badge-pill= @group.shared_with_group_links.count
- if show_invited_members
%li.nav-item
- = link_to "#invited_members", class: ["nav-link", ("active" if pending_active)], 'data-toggle' => 'tab' do
+ = link_to '#tab-invited-members', class: ['nav-link', ('active' if invited_active)], data: { toggle: 'tab' } do
%span
- = _("Pending")
+ = _('Invited')
%span.badge.badge-pill= @invited_members.total_count
-
+ - if show_access_requests
+ %li.nav-item
+ = link_to '#tab-access-requests', class: 'nav-link', data: { toggle: 'tab' } do
+ %span
+ = _('Access requests')
+ %span.badge.badge-pill= @requesters.count
.tab-content
- #existing_shares.tab-pane{ :class => ("active" unless pending_active) }
- - if @group.shared_with_group_links.any?
- .card.card-without-border
- .d-flex.flex-column.flex-md-row.row-content-block.second-block
- %span.flex-grow-1.align-self-md-center.col-form-label
- = _("Groups with access to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- %ul.content-list.members-list{ data: { qa_selector: "groups_list" } }
- - can_admin_member = can?(current_user, :admin_group_member, @group)
- - @group.shared_with_group_links.each do |group_link|
- = render 'shared/members/group', group_link: group_link, can_admin_member: can_admin_member, group_link_path: group_group_link_path(@group, group_link)
+ #tab-members.tab-pane{ class: ('active' unless invited_active) }
.card.card-without-border
- .d-flex.flex-column.flex-md-row.row-content-block.second-block
- %span.flex-grow-1.align-self-md-center.col-form-label
- = _("Members with access to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- = form_tag group_group_members_path(@group), method: :get, class: 'form-inline user-search-form' do
- .form-group.flex-grow
- .position-relative.mr-md-2
- = search_field_tag :search, params[:search], { placeholder: _('Search'), class: 'form-control', spellcheck: false }
- %button.user-search-btn.border-left{ type: "submit", "aria-label" => _("Submit search") }
- = icon("search")
- - if can_manage_members
- = label_tag '2fa', '2FA', class: 'col-form-label label-bold pr-md-2'
+ = render 'groups/group_members/tab_pane/header' do
+ = render 'groups/group_members/tab_pane/title' do
+ = html_escape(_('Members with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form gl-display-flex gl-md-align-items-center gl-flex-wrap gl-flex-direction-column gl-md-flex-direction-row gl-mx-n3 gl-my-n3', data: { testid: 'user-search-form' } do
+ .gl-px-3.gl-py-2
+ .search-control-wrap.gl-relative
+ = render 'shared/members/search_field'
+ - if can_manage_members
+ = render 'groups/group_members/tab_pane/form_item' do
+ = label_tag '2fa', _('2FA'), class: form_item_label_css_class
= render 'shared/members/filter_2fa_dropdown'
+ = render 'groups/group_members/tab_pane/form_item' do
+ = label_tag :sort_by, _('Sort by'), class: form_item_label_css_class
= render 'shared/members/sort_dropdown'
- %ul.content-list.members-list{ data: { qa_selector: "members_list" } }
+ %ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
= render partial: 'shared/members/member', collection: @members, as: :member
- = paginate @members, theme: 'gitlab'
-
+ = paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil }
+ - if @group.shared_with_group_links.any?
+ #tab-groups.tab-pane
+ .card.card-without-border
+ = render 'groups/group_members/tab_pane/header' do
+ = render 'groups/group_members/tab_pane/title' do
+ = html_escape(_('Groups with access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ %ul.content-list.members-list{ data: { qa_selector: 'groups_list' } }
+ - @group.shared_with_group_links.each do |group_link|
+ = render 'shared/members/group', group_link: group_link, can_admin_member: can_manage_members, group_link_path: group_group_link_path(@group, group_link)
- if show_invited_members
- #invited_members.tab-pane{ :class => ("active" if pending_active) }
+ #tab-invited-members.tab-pane{ class: ('active' if invited_active) }
.card.card-without-border
- .d-flex.flex-column.flex-md-row.row-content-block.second-block
- %span.flex-grow-1
- = _("Members with pending access to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- = form_tag group_group_members_path(@group), method: :get, class: 'form-inline user-search-form' do
- .form-group
- .position-relative.mr-md-2
- = search_field_tag :search_invited, params[:search_invited], { placeholder: _('Search'), class: 'form-control', spellcheck: false }
- %button.user-search-btn.border-left{ type: "submit", "aria-label" => _("Submit search") }
- = icon("search")
+ = render 'groups/group_members/tab_pane/header' do
+ = render 'groups/group_members/tab_pane/title' do
+ = html_escape(_('Members invited to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = form_tag group_group_members_path(@group), method: :get, class: 'user-search-form', data: { testid: 'user-search-form' } do
+ = render 'shared/members/search_field', name: 'search_invited'
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @invited_members, as: :member
- = paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab'
+ = paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab', params: { page: nil }
+ - if show_access_requests
+ #tab-access-requests.tab-pane
+ .card.card-without-border
+ = render 'groups/group_members/tab_pane/header' do
+ = render 'groups/group_members/tab_pane/title' do
+ = html_escape(_('Users requesting access to %{strong_start}%{group_name}%{strong_end}')) % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ %ul.content-list.members-list
+ = render partial: 'shared/members/member', collection: @requesters, as: :member
diff --git a/app/views/groups/group_members/tab_pane/_form_item.html.haml b/app/views/groups/group_members/tab_pane/_form_item.html.haml
new file mode 100644
index 00000000000..9e57d3329d7
--- /dev/null
+++ b/app/views/groups/group_members/tab_pane/_form_item.html.haml
@@ -0,0 +1,2 @@
+.gl-px-3.gl-py-3.gl-display-flex.gl-flex-direction-column.gl-md-flex-direction-row
+ = yield
diff --git a/app/views/groups/group_members/tab_pane/_header.html.haml b/app/views/groups/group_members/tab_pane/_header.html.haml
new file mode 100644
index 00000000000..a02bf90eddf
--- /dev/null
+++ b/app/views/groups/group_members/tab_pane/_header.html.haml
@@ -0,0 +1,2 @@
+.gl-display-flex.gl-md-align-items-center.gl-flex-direction-column.gl-md-flex-direction-row.row-content-block.second-block
+ = yield
diff --git a/app/views/groups/group_members/tab_pane/_title.html.haml b/app/views/groups/group_members/tab_pane/_title.html.haml
new file mode 100644
index 00000000000..c1418a5f7c8
--- /dev/null
+++ b/app/views/groups/group_members/tab_pane/_title.html.haml
@@ -0,0 +1,2 @@
+%span.gl-flex-grow-1.gl-py-3.gl-pr-3
+ = yield
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 59432e5f015..1358e848154 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -4,7 +4,7 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")
-- if group_issues_count(state: 'all').zero?
+- if group_issues_count(state: 'all') == 0
= render 'shared/empty_states/issues', project_select_button: true
- else
.top-area
@@ -25,7 +25,7 @@
- if Feature.enabled?(:vue_issuables_list, @group)
.js-issuables-list{ data: { endpoint: expose_url(api_v4_groups_issues_path(id: @group.id)),
'can-bulk-edit': @can_bulk_update.to_json,
- 'empty-svg-path': image_path('illustrations/issues.svg'),
+ 'empty-state-meta': { svg_path: image_path('illustrations/issues.svg') },
'sort-key': @sort } }
- else
= render 'shared/issues'
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 1828f850d35..15e777f5c36 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -2,7 +2,7 @@
- page_title _("Merge Requests")
-- if group_merge_requests_count(state: 'all').zero?
+- if group_merge_requests_count(state: 'all') == 0
= render 'shared/empty_states/merge_requests', project_select_button: true
- else
.top-area
diff --git a/app/views/groups/packages/_legacy_package_list.haml b/app/views/groups/packages/_legacy_package_list.haml
new file mode 100644
index 00000000000..481a0dbb6e8
--- /dev/null
+++ b/app/views/groups/packages/_legacy_package_list.haml
@@ -0,0 +1,59 @@
+- sort_value = @sort
+- sort_title = packages_sort_option_title(sort_value)
+
+- if @packages.any?
+ .d-flex.justify-content-end
+ .dropdown.inline.gl-mt-3.gl-mb-3.package-sort-dropdown
+ .btn-group{ role: 'group' }
+ .btn-group{ role: 'group' }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static', 'qa-selector': 'sort-dropdown-button' }, class: 'btn btn-default' }
+ = sort_title
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
+ %li
+ = sortable_item(sort_title_created_date, package_sort_path(sort: sort_value_recently_created), sort_title)
+ = sortable_item(sort_title_name, package_sort_path(sort: sort_value_name_desc), sort_title)
+ = sortable_item(sort_title_project_name, package_sort_path(sort: sort_value_project_name_desc), sort_title)
+ = sortable_item(sort_title_version, package_sort_path(sort: sort_value_version_desc), sort_title)
+ = sortable_item(sort_title_type, package_sort_path(sort: sort_value_type_desc), sort_title)
+ = packages_sort_direction_button(sort_value)
+
+ .table-holder
+ .gl-responsive-table-row.table-row-header.bg-secondary-50.px-2.border-top{ role: 'row' }
+ .table-section.section-30{ role: 'rowheader' }
+ = _('Name')
+ .table-section.section-20{ role: 'rowheader' }
+ = _('Project')
+ .table-section.section-20{ role: 'rowheader' }
+ = _('Version')
+ .table-section.section-10{ role: 'rowheader' }
+ = _('Type')
+ .table-section.section-20{ role: 'rowheader' }
+ = _('Created')
+ - @packages.each do |package|
+ .gl-responsive-table-row{ data: { 'qa-selector': 'package-row' } }
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= _("Name")
+ .table-mobile-content.flex-truncate-parent
+ = link_to package.name, project_package_path(package.project, package), class: 'flex-truncate-child'
+ .table-section.section-20
+ .table-mobile-header{ role: "rowheader" }= _("Project")
+ .table-mobile-content
+ = link_to_project(package.project)
+ .table-section.section-20
+ .table-mobile-header{ role: "rowheader" }= _("Version")
+ .table-mobile-content
+ = package.version
+ .table-section.section-10
+ .table-mobile-header{ role: "rowheader" }= _("Type")
+ .table-mobile-content
+ = package.package_type
+ .table-section.section-20
+ .table-mobile-header{ role: "rowheader" }= _("Created")
+ .table-mobile-content
+ = time_ago_with_tooltip(package.created_at)
+ = paginate @packages, theme: "gitlab"
+- else
+ .row.empty-state
+ .col-12
+ = render 'shared/packages/no_packages'
diff --git a/app/views/groups/packages/index.html.haml b/app/views/groups/packages/index.html.haml
new file mode 100644
index 00000000000..b07c08f50ca
--- /dev/null
+++ b/app/views/groups/packages/index.html.haml
@@ -0,0 +1,5 @@
+- page_title _("Packages")
+
+.row
+ .col-12
+ #js-vue-packages-list{ data: packages_list_data('groups', @group) }
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index bf9d89da24a..555c4004a3f 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -15,7 +15,7 @@
.controls
= link_to _('Members'), project_project_members_path(project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to _('Edit'), edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn"
- = link_to _('Remove'), project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-remove"
+ = link_to _('Delete'), project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-remove"
.stats
%span.badge.badge-pill
diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml
index df615eb189a..07cbcd8401e 100644
--- a/app/views/groups/runners/_runner.html.haml
+++ b/app/views/groups/runners/_runner.html.haml
@@ -68,14 +68,14 @@
.btn-group.table-action-buttons
.btn-group
= link_to edit_group_runner_path(@group, runner), class: 'btn btn-default has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do
- = icon('pencil')
+ = sprite_icon('pencil')
.btn-group
- if runner.active?
= link_to pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-default has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do
- = icon('pause')
+ = sprite_icon('pause')
- else
= link_to resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-default has-tooltip', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do
- = icon('play')
+ = sprite_icon('play')
- if runner.belongs_to_more_than_one_project?
.btn-group
.btn.btn-danger.has-tooltip{ 'aria-label' => 'Remove', 'data-container' => 'body', 'data-original-title' => _('Multi-project Runners cannot be removed'), 'data-placement' => 'top', disabled: 'disabled' }
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
index 0df82898644..98f4acaa5e3 100644
--- a/app/views/groups/settings/_advanced.html.haml
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -1,12 +1,12 @@
= render 'groups/settings/export', group: @group
.sub-section
- %h4.warning-title= s_('GroupSettings|Change group path')
+ %h4.warning-title= s_('GroupSettings|Change group URL')
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@group)
.form-group
%p
- = s_('GroupSettings|Changing group path can have unintended side effects.')
+ = s_('GroupSettings|Changing group URL can have unintended side effects.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
@@ -20,10 +20,10 @@
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
- title: s_('GroupSettings|Please choose a group path with no special characters.'),
+ title: s_('GroupSettings|Please choose a group URL with no special characters.'),
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
-
- = f.submit s_('GroupSettings|Change group path'), class: 'btn btn-warning'
+ .gl-display-flex.gl-justify-content-end
+ = f.submit s_('GroupSettings|Change group URL'), class: 'btn btn-warning'
.sub-section
%h4.warning-title= s_('GroupSettings|Transfer group')
@@ -39,7 +39,8 @@
%li= s_('GroupSettings|You can only transfer the group to a group you manage.')
%li= s_('GroupSettings|You will need to update your local repositories to point to the new location.')
%li= s_("GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.")
- = f.submit s_('GroupSettings|Transfer group'), class: 'btn btn-warning'
+ .gl-display-flex.gl-justify-content-end
+ = f.submit s_('GroupSettings|Transfer group'), class: 'btn btn-warning'
= render 'groups/settings/remove', group: @group
= render_if_exists 'groups/settings/restore', group: @group
diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml
index 94466b76ac8..af06cfff397 100644
--- a/app/views/groups/settings/_export.html.haml
+++ b/app/views/groups/settings/_export.html.haml
@@ -24,5 +24,6 @@
= link_to _('Download export'), download_export_group_path(group),
rel: 'nofollow', method: :get, class: 'btn btn-default', data: { qa_selector: 'download_export_link' }
- else
- = link_to _('Export group'), export_group_path(group),
- method: :post, class: 'btn btn-default', data: { qa_selector: 'export_group_link' }
+ .gl-display-flex.gl-justify-content-end
+ = link_to _('Export group'), export_group_path(group),
+ method: :post, class: 'btn btn-default', data: { qa_selector: 'export_group_link' }
diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml
index 0094104e07d..e43d49b229e 100644
--- a/app/views/groups/settings/_general.html.haml
+++ b/app/views/groups/settings/_general.html.haml
@@ -19,7 +19,7 @@
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
- .form-group.gl-mt-3.append-bottom-20
+ .form-group.gl-mt-3.gl-mb-6
.avatar-container.rect-avatar.s90
= group_icon(@group, alt: '', class: 'avatar group-avatar s90')
= f.label :avatar, _('Group avatar'), class: 'label-bold d-block'
@@ -29,5 +29,5 @@
= link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link'
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
-
- = f.submit _('Save changes'), class: 'btn btn-success mt-4 js-dirty-submit', data: { qa_selector: 'save_name_visibility_settings_button' }
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: 'btn btn-success mt-4 js-dirty-submit', data: { qa_selector: 'save_name_visibility_settings_button' }
diff --git a/app/views/groups/settings/_pages_settings.html.haml b/app/views/groups/settings/_pages_settings.html.haml
index 9e1932185da..b6cf05d96ab 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|
= render_if_exists 'shared/pages/max_pages_size_input', form: f
- .prepend-top-10
+ .gl-mt-3
= f.submit s_('GitLabPages|Save'), class: 'btn btn-success'
diff --git a/app/views/groups/settings/_permanent_deletion.html.haml b/app/views/groups/settings/_permanent_deletion.html.haml
index 155efc03ffe..063ff6dd132 100644
--- a/app/views/groups/settings/_permanent_deletion.html.haml
+++ b/app/views/groups/settings/_permanent_deletion.html.haml
@@ -5,5 +5,5 @@
= _('Removing this group also removes all child projects, including archived projects, and their resources.')
%br
%strong= _('Removed group can not be restored!')
-
- = button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(group) }
+ .gl-display-flex.gl-justify-content-end
+ = button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(group) }
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 507246d573e..86f49672d66 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -37,8 +37,9 @@
= render 'groups/settings/default_branch_protection', f: f, group: @group
= render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_creation_level', f: f, group: @group
+ = render_if_exists 'groups/settings/prevent_forking', f: f, group: @group
= render 'groups/settings/two_factor_auth', f: f
= render_if_exists 'groups/personal_access_token_expiration_policy', f: f, group: @group
= render_if_exists 'groups/member_lock_setting', f: f, group: @group
-
- = f.submit _('Save changes'), class: 'btn btn-success gl-mt-3 js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: 'btn btn-success gl-mt-3 js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }
diff --git a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
index e7efc0237c8..2b5019222f8 100644
--- a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
+++ b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
@@ -12,4 +12,4 @@
.form-text.text-muted
= s_('GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found.')
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank'
- = f.submit _('Save changes'), class: 'btn btn-success prepend-top-15'
+ = f.submit _('Save changes'), class: 'btn btn-success gl-mt-5'
diff --git a/app/views/groups/sidebar/_packages.html.haml b/app/views/groups/sidebar/_packages.html.haml
index 59061a048b3..54510d5df0c 100644
--- a/app/views/groups/sidebar/_packages.html.haml
+++ b/app/views/groups/sidebar/_packages.html.haml
@@ -1,16 +1,23 @@
-- if group_container_registry_nav?
- = nav_link(controller: 'groups/registry/repositories') do
- = link_to group_container_registries_path(@group), title: _('Container Registry') do
+- packages_link = group_packages_list_nav? ? group_packages_path(@group) : group_container_registries_path(@group)
+
+- if group_packages_nav?
+ = nav_link(controller: ['groups/packages', 'groups/registry/repositories']) do
+ = link_to packages_link, title: _('Packages') do
.nav-icon-container
= sprite_icon('package')
%span.nav-item-name
= _('Packages & Registries')
%ul.sidebar-sub-level-items
- = nav_link(controller: 'groups/registry/repositories', html_options: { class: "fly-out-top-item" } ) do
- = link_to group_container_registries_path(@group), title: _('Container Registry') do
+ = nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do
+ = link_to packages_link, title: _('Packages & Registries') do
%strong.fly-out-top-item-name
= _('Packages & Registries')
%li.divider.fly-out-top-item
- = nav_link(controller: 'groups/registry/repositories') do
- = link_to group_container_registries_path(@group), title: _('Container Registry') do
- %span= _('Container Registry')
+ - if group_packages_list_nav?
+ = nav_link(controller: 'groups/packages') do
+ = link_to group_packages_path(@group), title: _('Packages') do
+ %span= _('Package Registry')
+ - if group_container_registry_nav?
+ = nav_link(controller: 'groups/registry/repositories') do
+ = link_to group_container_registries_path(@group), title: _('Container Registry') do
+ %span= _('Container Registry')
diff --git a/app/views/help/instance_configuration/_ssh_info.html.haml b/app/views/help/instance_configuration/_ssh_info.html.haml
index a7ee37b2784..2c6ada4d3f2 100644
--- a/app/views/help/instance_configuration/_ssh_info.html.haml
+++ b/app/views/help/instance_configuration/_ssh_info.html.haml
@@ -9,7 +9,7 @@
- if ssh_info.blank?
%p
- = _('SSH host keys are not available on this system. Please use <code>ssh-keyscan</code> command or contact your GitLab administrator for more information.').html_safe
+ = html_escape(_('SSH host keys are not available on this system. Please use %{ssh_keyscan} command or contact your GitLab administrator for more information.')) % { ssh_keyscan: tag.code('ssh-keyscan') }
- else
%p
= _('Below are the fingerprints for the current instance SSH host keys.')
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
deleted file mode 100644
index 5c216ee1ec0..00000000000
--- a/app/views/help/ui.html.haml
+++ /dev/null
@@ -1,524 +0,0 @@
-- page_title _("UI Development Kit"), _("Help")
-- lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare."
-- link_classes = "flex-grow-1 mx-1 "
-
-.gitlab-ui-dev-kit
- %h1 GitLab UI development kit
- %p.light
- Use page inspector in your browser to check element classes and structure
- of examples below.
- %hr
- %ul
- %li
- = link_to 'Blocks', '#blocks'
- %li
- = link_to 'Lists', '#lists'
- %li
- = link_to 'Tables', '#tables'
- %li
- = link_to 'Nav', '#nav'
- %li
- = link_to 'Buttons', '#buttons'
- %li
- = link_to 'Dropdowns', '#dropdowns'
- %li
- = link_to 'Panels', '#panels'
- %li
- = link_to 'Alerts', '#alerts'
- %li
- = link_to 'Forms', '#forms'
- %li
- = link_to 'Files', '#file'
- %li
- = link_to 'Markdown', '#markdown'
-
- %h2#blocks Blocks
-
- .lead
- Content block separated with botton border
- %code .content-block
-
- .example
- .content-block
- %h4 Normal block inside content
- = lorem
-
- .content-block
- %h4 Second block
- = lorem
-
- .lead
- Gray content block with side padding using
- %code .row-content-block
-
- .example
- .row-content-block
- %h4 Normal block inside content
- = lorem
-
- .row-content-block.second-block
- %h4 Second block
- = lorem
-
-
- .lead
- Cover block for profile page with avatar, name and description
- %code .cover-block
- .example
- .cover-block.user-cover-block
- = render layout: 'users/cover_controls' do
- = link_to '#', class: link_classes + 'btn btn-default' do
- = icon('pencil')
- = link_to '#', class: link_classes + 'btn btn-default' do
- = icon('rss')
- .avatar-holder
- = image_tag avatar_icon_for_email('admin@example.com', 90), class: "avatar s90", alt: ''
- .cover-title
- John Smith
-
- .cover-desc.cgray
- = lorem
-
- %h2#lists Lists
-
- .lead
- Simple list using
- %code .content-list
-
- .example
- %ul.content-list
- %li
- One item
- %li
- One item
- %li
- One item
-
- .lead
- List with avatar, title and description using
- %code .content-list
-
- .example
- %ul.content-list
- %li
- = image_tag 'no_avatar.png', class: 'avatar s40'
- .title Title
- .description Description
- %li
- = image_tag 'no_avatar.png', class: 'avatar s40'
- .title Title
- .description Description
- %li
- = image_tag 'no_avatar.png', class: 'avatar s40'
- .title Title
- .description Description
-
- .lead
- List with hover effect
- %code .hover-list
- .example
- %ul.hover-list
- %li
- One item
- %li
- One item
- %li
- One item
-
- .lead
- List inside panel
- .example
- .card
- .card-header Your list
- %ul.content-list
- %li
- One item
- %li
- One item
- %li
- One item
-
- %h2#tables Tables
-
- .example
- %table.table
- %thead
- %tr
- %th #
- %th First Name
- %th Last Name
- %th Username
- %tbody
- %tr
- %td 1
- %td Mark
- %td Otto
- %td @mdo
- %tr
- %td 2
- %td Jacob
- %td Thornton
- %td @fat
- %tr
- %td 3
- %td Larry
- %td the Bird
- %td @twitter
-
- %h2#navs Navigation
-
- .lead
- Holder for top page navigation. Includes navigation, search field, sorting and button
- %code .top-area
-
- .example
- .top-area
- %ul.nav-links.nav.nav-tabs
- %li.active
- %a Open
- %li
- %a Closed
- .nav-controls
- = text_field_tag 'sample', nil, class: 'form-control'
- .dropdown
- %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span Sort by name
- = icon('chevron-down')
- %ul.dropdown-menu
- %li
- = link_to 'Sort by date', '#'
-
- = link_to 'New issue', '#', class: 'btn btn-success btn-inverted'
-
- .lead
- Only nav links without button and search
- %code .nav-links
- .example
- %ul.nav-links
- %li.active
- %a Open
- %li
- %a Closed
-
-
- %h2#buttons Buttons
-
- .example
- %button.btn.btn-default{ :type => "button" } Secondary
- %button.btn.btn-primary{ :type => "button" } Primary
- %button.btn.btn-success{ :type => "button" } Success
- %button.btn.btn-info{ :type => "button" } Info
- %button.btn.btn-warning{ :type => "button" } Warning
- %button.btn.btn-danger{ :type => "button" } Danger
- %button.btn.btn-link{ :type => "button" } Link
-
- %h2#dropdowns Dropdowns
-
- .example
- .clearfix
- .dropdown.inline.float-left
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- Dropdown
- = icon('chevron-down')
- %ul.dropdown-menu
- %li
- %a{ href: "#" }
- Dropdown option
- .dropdown.inline.float-right
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- Dropdown
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-right
- %li
- %a{ href: "#" }
- Dropdown option
- .example
- %div
- .dropdown.inline
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- Dropdown
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-selectable
- %li
- %a.is-active{ href: "#" }
- Dropdown option
- .example
- %div
- .dropdown.inline
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- Dropdown
- = icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-selectable
- .dropdown-title
- %span Dropdown title
- %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
- = icon('times')
- .dropdown-input
- %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
- = icon('search')
- .dropdown-content
- %ul
- %li
- %a.is-active{ href: "#" }
- Dropdown option
- %li
- %a{ href: "#" }
- Dropdown option
- %li.divider
- %li
- %a{ href: "#" }
- Dropdown option
- %li
- %a{ href: "#" }
- Dropdown option
- %li
- %a{ href: "#" }
- Dropdown option
- %li
- %a{ href: "#" }
- Dropdown option
- %li
- %a{ href: "#" }
- Dropdown option
- .dropdown-footer
- %strong Tip:
- If an author is not a member of this project, you can still filter by their name while using the search field.
- .dropdown.inline
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- Dropdown loading
- = icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
- .dropdown-title
- %span Dropdown title
- %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
- = icon('times')
- .dropdown-input
- %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
- = icon('search')
- .dropdown-content
- %ul
- %li
- %a.is-active{ href: "#" }
- Dropdown option
- %li
- %a{ href: "#" }
- Dropdown option
- %li.divider
- %li
- %a{ href: "#" }
- Dropdown option
- %li
- %a{ href: "#" }
- Dropdown option
- %li
- %a{ href: "#" }
- Dropdown option
- %li
- %a{ href: "#" }
- Dropdown option
- %li
- %a{ href: "#" }
- Dropdown option
- .dropdown-footer
- %strong Tip:
- If an author is not a member of this project, you can still filter by their name while using the search field.
- .dropdown-loading.text-center
- .spinner.spinner-md.mt-8
-
- .example
- %div
- .dropdown.inline
- %button.dropdown-menu-toggle{ type: 'button', data: {toggle: 'dropdown' } }
- Dropdown user
- = icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
- .dropdown-title
- %span Dropdown title
- %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
- = icon('times')
- .dropdown-input
- %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
- = icon('search')
- .dropdown-content
- %ul
- %li
- %a.dropdown-menu-user-link.is-active{ href: "#" }
- = link_to_member_avatar(@user, size: 30)
- %strong.dropdown-menu-user-full-name
- = @user.name
- .dropdown-menu-user-username
- = @user.to_reference
-
- .example
- %div
- .dropdown.inline
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- Dropdown page 2
- = icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user.dropdown-menu-paging.is-page-two
- .dropdown-page-one
- .dropdown-title
- %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } }
- = icon('arrow-left')
- %span Dropdown title
- %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
- = icon('times')
- .dropdown-input
- %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
- = icon('search')
- .dropdown-content
- %ul
- %li
- %a.dropdown-menu-user-link.is-active{ href: "#" }
- = link_to_member_avatar(@user, size: 30)
- %strong.dropdown-menu-user-full-name
- = @user.name
- .dropdown-menu-user-username
- = @user.to_reference
- .dropdown-page-two
- .dropdown-title
- %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } }
- = icon('arrow-left')
- %span Create label
- %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
- = icon('times')
- .dropdown-input
- %input.dropdown-input-field{ type: "search", placeholder: "Name new label" }
- .dropdown-content
- %button.btn.btn-primary
- Create
-
- .example
- %div
- .dropdown.inline
- %button#js-project-dropdown.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- Projects
- = icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-selectable
- .dropdown-title
- %span Go to project
- %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
- = icon('times')
- .dropdown-input
- %input.dropdown-input-field{ type: "search", placeholder: "Filter results" }
- = icon('search')
- .dropdown-content
- .dropdown-loading.text-center
- .spinner.spinner-md.mt-8
-
- .example
- %div
- = dropdown_tag("Projects", options: { title: "Go to project", filter: true, placeholder: "Filter projects" })
-
- %h2#panels Panels
-
- .row
- .col-md-6
- .card.bg-success
- .card-header Success
- .card-body
- = lorem
- .card.bg-primary
- .card-header Primary
- .card-body
- = lorem
- .card.bg-info
- .card-header Info
- .card-body
- = lorem
- .col-md-6
- .card.bg-warning
- .card-header Warning
- .card-body
- = lorem
- .card.bg-danger
- .card-header Danger
- .card-body
- = lorem
-
- %h2#alerts Alerts
-
- .row
- .col-md-6
- .alert.alert-success
- = lorem
- .alert.alert-info
- = lorem
- .col-md-6
- .alert.alert-warning
- = lorem
- .alert.alert-danger
- = lorem
-
- %h2#forms Forms
-
- .lead
- Horizontal form when label rendered inline with input
- %code form.horizontal-form
-
- .example
- %form
- .form-group.row
- %label.col-sm-2.col-form-label{ :for => "inputEmail3" } Email
- .col-sm-10
- %input#inputEmail3.form-control{ :placeholder => "Email", :type => "email" }/
- .form-group.row
- %label.col-sm-2.col-form-label{ :for => "inputPassword3" } Password
- .col-sm-10
- %input#inputPassword3.form-control{ :placeholder => "Password", :type => "password" }/
- .form-group.row
- .offset-sm-2.col-sm-10
- .form-check
- %input.form-check-input{ :type => "checkbox" }/
- %label.form-check-label
- Remember me
- .form-group.row
- .offset-sm-2.col-sm-10
- %button.btn.btn-default{ :type => "submit" } Sign in
-
- .lead
- Form when label rendered above input
- %code form
-
- .example
- %form
- .form-group
- %label{ :for => "exampleInputEmail1" } Email address
- %input#exampleInputEmail1.form-control{ :placeholder => "Enter email", :type => "email" }/
- .form-group
- %label{ :for => "exampleInputPassword1" } Password
- %input#exampleInputPassword1.form-control{ :placeholder => "Password", :type => "password" }/
- .form-check
- %input.form-check-input{ :type => "checkbox" }/
- %label.form-check-label
- Remember me
- %button.btn.btn-default{ :type => "submit" } Sign in
-
- %h2#file File
- %h4
- %code .file-holder
-
- - blob = Snippet.new(content: "Wow\nSuch\nFile").blob
- .example
- .file-holder
- .js-file-title.file-title
- Awesome file
- .file-actions
- .btn-group
- %a.btn Edit
- %a.btn.btn-danger Remove
- = render 'shared/file_highlight', blob: blob
-
- %h2#markdown Markdown
- %h4
- %code .md
-
- Markdown rendering has a bit different css and presented in next UI elements:
-
- %ul
- %li comment
- %li issue, merge request description
- %li wiki page
- %li help page
-
- You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("user/markdown")}.
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index 9bf1f0c61bb..fca73f118b3 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -1,12 +1,15 @@
- provider = local_assigns.fetch(:provider)
- extra_data = local_assigns.fetch(:extra_data, {})
- filterable = local_assigns.fetch(:filterable, true)
+- paginatable = local_assigns.fetch(:paginatable, false)
- provider_title = Gitlab::ImportSources.title(provider)
#import-projects-mount-element{ data: { provider: provider, provider_title: provider_title,
can_select_namespace: current_user.can_select_namespace?.to_s,
ci_cd_only: has_ci_cd_only_params?.to_s,
+ namespaces_path: import_available_namespaces_path,
repos_path: url_for([:status, :import, provider, format: :json]),
jobs_path: url_for([:realtime_changes, :import, provider, format: :json]),
import_path: url_for([:import, provider, format: :json]),
- filterable: filterable.to_s }.merge(extra_data) }
+ filterable: filterable.to_s,
+ paginatable: paginatable.to_s }.merge(extra_data) }
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index a24a1c1fb05..b3ca1beb853 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -5,4 +5,4 @@
%i.fa.fa-bitbucket-square
= _('Import projects from Bitbucket Server')
-= render 'import/githubish_status', provider: 'bitbucket_server', extra_data: { reconfigure_path: configure_import_bitbucket_server_path }
+= render 'import/githubish_status', provider: 'bitbucket_server', paginatable: true, extra_data: { reconfigure_path: configure_import_bitbucket_server_path }
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index 8ed9dc68bb3..cdc53520e93 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -18,7 +18,7 @@
%li
%strong= _("Map a FogBugz account ID to a GitLab user")
%p
- = _('Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. "By <a href="#">@johnsmith</a>"). It will also associate and/or assign these issues and comments with the selected user.').html_safe
+ = 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
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index 5513849be3d..ef803a36e79 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -1,7 +1,7 @@
- page_title _("GitLab.com import")
- header_title _("Projects"), root_path
%h3.page-title
- = sprite_icon('heart', size: 16, css_class: 'gl-vertical-align-middle')
+ = sprite_icon('heart', css_class: 'gl-vertical-align-middle')
= _('Import projects from GitLab.com')
= render 'import/githubish_status', provider: 'gitlab', filterable: false
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index b667d2aa0d7..cd477c085f9 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -3,7 +3,7 @@
%h3.page-title.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
- = sprite_icon('tanuki', size: 16, css_class: 'gl-mr-2')
+ = sprite_icon('tanuki', css_class: 'gl-mr-2')
= _('Import an exported GitLab project')
%hr
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index 7a6ad28f0aa..7dec67191b9 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -19,31 +19,31 @@
= _("Make sure you're logged into the account that owns the projects you'd like to import.")
%li
%p
- = _('Click the <strong>Select none</strong> button on the right, since we only need "Google Code Project Hosting".').html_safe
+ = html_escape(_('Click the %{strong_open}Select none%{strong_close} button on the right, since we only need "Google Code Project Hosting".')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%li
%p
- = _('Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right.').html_safe
+ = html_escape(_('Scroll down to %{strong_open}Google Code Project Hosting%{strong_close} and enable the switch on the right.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%li
%p
- = _('Choose <strong>Next</strong> at the bottom of the page.').html_safe
+ = html_escape(_('Choose %{strong_open}Next%{strong_close} at the bottom of the page.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%li
%p
= _('Leave the "File type" and "Delivery method" options on their default values.')
%li
%p
- = _('Choose <strong>Create archive</strong> and wait for archiving to complete.').html_safe
+ = html_escape(_('Choose %{strong_open}Create archive%{strong_close} and wait for archiving to complete.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%li
%p
- = _('Click the <strong>Download</strong> button and wait for downloading to complete.').html_safe
+ = html_escape(_('Click the %{strong_open}Download%{strong_close} button and wait for downloading to complete.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%li
%p
= _('Find the downloaded ZIP file and decompress it.')
%li
%p
- = _('Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file.').html_safe
+ = html_escape(_('Find the newly extracted %{code_open}Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json%{code_close} file.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
%p
- = _('Upload <code>GoogleCodeProjectHosting.json</code> here:').html_safe
+ = html_escape(_('Upload %{code_open}GoogleCodeProjectHosting.json%{code_close} here:')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%p
%input{ type: "file", name: "dump_file", id: "dump_file" }
%li
@@ -57,6 +57,6 @@
= label_tag :create_user_map_1 do
= radio_button_tag :create_user_map, 1, false
= _('Yes, let me map Google Code users to full names or GitLab users.')
- %li
- %p
- = submit_tag _('Continue to the next step'), class: "btn btn-success"
+
+ %span
+ = submit_tag _('Continue to the next step'), class: "btn btn-success"
diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml
index 732ba95a63f..1f1bfda7ee4 100644
--- a/app/views/import/google_code/new_user_map.html.haml
+++ b/app/views/import/google_code/new_user_map.html.haml
@@ -9,24 +9,24 @@
%p
= _("Customize how Google Code email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import.")
%p
- = _("The user map is a JSON document mapping the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of <code>:</code>. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side.").html_safe
+ = html_escape(_("The user map is a JSON document mapping the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of %{code_open}:%{code_close}. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%ul
%li
%strong= _("Default: Directly import the Google Code email address or username")
%p
- = _('<code>"johnsmith@example.com": "johnsm...@example.com"</code> will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com. The email address or username is masked to ensure the user\'s privacy.').html_safe
+ = html_escape(_('%{code_open}"johnsmith@example.com": "johnsm...@example.com"%{code_close} will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com. The email address or username is masked to ensure the user\'s privacy.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
%strong= _("Map a Google Code user to a GitLab user")
%p
- = _('<code>"johnsmith@example.com": "@johnsmith"</code> will add "By <a href="#">@johnsmith</a>" to all issues and comments originally created by johnsmith@example.com, and will set <a href="#">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com.').html_safe
+ = html_escape(_('%{code_open}"johnsmith@example.com": "@johnsmith"%{code_close} will add "By %{link_open}@johnsmith%{link_close}" to all issues and comments originally created by johnsmith@example.com, and will set %{link_open}@johnsmith%{link_close} as the assignee on all issues originally assigned to johnsmith@example.com.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe, link_open: '<a href="#">'.html_safe, link_close: '</a>'.html_safe }
%li
%strong= _("Map a Google Code user to a full name")
%p
- = _('<code>"johnsmith@example.com": "John Smith"</code> will add "By John Smith" to all issues and comments originally created by johnsmith@example.com.').html_safe
+ = html_escape(_('%{code_open}"johnsmith@example.com": "John Smith"%{code_close} will add "By John Smith" to all issues and comments originally created by johnsmith@example.com.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
%strong= _("Map a Google Code user to a full email address")
%p
- = _('<code>"johnsmith@example.com": "johnsmith@example.com"</code> will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user\'s privacy. Use this option if you want to show the full email address.').html_safe
+ = html_escape(_('%{code_open}"johnsmith@example.com": "johnsmith@example.com"%{code_close} will add "By %{link_open}johnsmith@example.com%{link_close}" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user\'s privacy. Use this option if you want to show the full email address.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe, link_open: '<a href="#">'.html_safe, link_close: '</a>'.html_safe }
.form-group.row
.col-sm-12
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index f322b7a956a..8d8754e1069 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -37,7 +37,7 @@
%td
= link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank", rel: 'noopener noreferrer'
%td
- = link_to project.full_path, [project.namespace.becomes(Namespace), project]
+ = link_to project.full_path, project
%td.job-status
- case project.import_status
- when 'finished'
diff --git a/app/views/import/manifest/_form.html.haml b/app/views/import/manifest/_form.html.haml
index b515ce084e4..3a5b5924c6a 100644
--- a/app/views/import/manifest/_form.html.haml
+++ b/app/views/import/manifest/_form.html.haml
@@ -18,6 +18,6 @@
= _('Import multiple repositories by uploading a manifest file.')
= link_to icon('question-circle'), help_page_path('user/project/import/manifest')
- .append-bottom-10
+ .gl-mb-3
= submit_tag _('List available repositories'), class: 'btn btn-success'
= link_to _('Cancel'), new_project_path, class: 'btn btn-cancel'
diff --git a/app/views/import/manifest/status.html.haml b/app/views/import/manifest/status.html.haml
index e85162ad1b4..c3e77554b09 100644
--- a/app/views/import/manifest/status.html.haml
+++ b/app/views/import/manifest/status.html.haml
@@ -1,42 +1,7 @@
- page_title _("Manifest import")
- header_title _("Projects"), root_path
-- provider = 'manifest'
%h3.page-title
= _('Manifest file import')
-%p
- = button_tag class: "btn btn-import btn-success js-import-all" do
- = _('Import all repositories')
- = icon("spinner spin", class: "loading-icon")
-
-.table-responsive
- %table.table.import-jobs
- %thead
- %tr
- %th= _('Repository URL')
- %th= _('To GitLab')
- %th= _('Status')
- %tbody
- - @already_added_projects.each do |project|
- %tr{ id: "project_#{project.id}", class: project_status_css_class(project.import_status) }
- %td
- = project.import_url
- %td
- = link_to_project project
- %td.job-status
- = render 'import/project_status', project: project
-
- - @pending_repositories.each do |repository|
- %tr{ id: "repo_#{repository[:id]}" }
- %td
- = repository[:url]
- %td.import-target
- = import_project_target(@group.full_path, repository[:path])
- %td.import-actions.job-status
- = button_tag class: "btn btn-import js-add-to-import" do
- = _('Import')
- = icon("spinner spin", class: "loading-icon")
-
-.js-importer-status{ data: { jobs_import_path: url_for([:jobs, :import, provider]),
- import_path: url_for([:import, provider]) } }
+= render 'import/githubish_status', provider: 'manifest'
diff --git a/app/views/import/phabricator/new.html.haml b/app/views/import/phabricator/new.html.haml
index 3dfc7c37d98..5f73a27dbd6 100644
--- a/app/views/import/phabricator/new.html.haml
+++ b/app/views/import/phabricator/new.html.haml
@@ -3,8 +3,10 @@
- breadcrumb_title title
- header_title _("Projects"), root_path
-%h3.page-title
- = icon 'issues', text: _('Import tasks from Phabricator into issues')
+%h3.page-title.d-flex
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('issues', css_class: 'gl-mr-2')
+ = _('Import tasks from Phabricator into issues')
= render 'import/shared/errors'
diff --git a/app/views/instance_statistics/dev_ops_score/_card.html.haml b/app/views/instance_statistics/dev_ops_score/_card.html.haml
index c63bd96a175..dd6e5c0f108 100644
--- a/app/views/instance_statistics/dev_ops_score/_card.html.haml
+++ b/app/views/instance_statistics/dev_ops_score/_card.html.haml
@@ -18,8 +18,8 @@
= number_to_percentage(card.percentage_score, precision: 1)
.board-card-buttons
- if card.blog
- %a{ href: card.blog }
- = icon('info-circle', 'aria-hidden' => 'true')
+ %a.btn-svg{ href: card.blog }
+ = sprite_icon('information-o')
- if card.docs
- %a{ href: card.docs }
- = icon('question-circle', 'aria-hidden' => 'true')
+ %a.btn-svg{ href: card.docs }
+ = sprite_icon('question-o')
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index 2bcd64d0690..283683511d7 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -2,28 +2,19 @@
%h3.page-title= _("Invitation")
%p
- You have been invited
- - if inviter = @member.created_by
- by
+ = _("You have been invited")
+ - inviter = @member.created_by
+ - if inviter
+ = _("by")
= link_to inviter.name, user_url(inviter)
- to join
- - case @member.source
- - when Project
- - project = @member.source
- project
- %strong
- = link_to project.full_name, project_url(project)
- - when Group
- - group = @member.source
- group
- %strong
- = link_to group.name, group_url(group)
- as #{@member.human_access}.
+ = _("to join %{source_name}") % { source_name: @invite_details[:title] }
+ %strong
+ = link_to @invite_details[:name], @invite_details[:url]
+ = _("as %{role}.") % { role: @member.human_access }
- if member?
%p
- - member_source = @member.source.is_a?(Group) ? _("group") : _("project")
- = _("However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation.") % { member_source: member_source }
+ = _("However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation.") % { member_source: @invite_details[:title] }
- if !current_user_matches_invite?
%p
@@ -32,7 +23,7 @@
- link_to_current_user = link_to(current_user.to_reference, user_url(current_user))
= _("Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}.").html_safe % { mail_to_invite_email: mail_to_invite_email, mail_to_current_user: mail_to_current_user, link_to_current_user: link_to_current_user }
-- unless member?
+- if !member?
.actions
= link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn btn-success"
= link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn btn-danger gl-ml-3"
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 07c271be2f0..be3f2fd74e4 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -6,7 +6,8 @@
.js-toast-message{ data: { message: value } }
- elsif value
%div{ class: "flash-#{key} mb-2" }
- = sprite_icon(icons[key], size: 16, css_class: 'align-middle mr-1') unless icons[key].nil?
+ = sprite_icon(icons[key], css_class: 'align-middle mr-1') unless icons[key].nil?
%span= value
- %div{ class: "close-icon-wrapper js-close-icon" }
- = sprite_icon('close', size: 16, css_class: 'close-icon')
+ - if %w(alert notice success).include?(key)
+ %div{ class: "close-icon-wrapper js-close-icon" }
+ = sprite_icon('close', css_class: 'close-icon')
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index d1311f17b72..b869298e99d 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -49,14 +49,17 @@
= favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png'
+ = render 'layouts/startup_css'
- if user_application_theme == 'gl-dark'
- = stylesheet_link_tag "application_dark", media: "all"
+ = stylesheet_link_tag_defer "application_dark"
- else
- = stylesheet_link_tag "application", media: "all"
+ = stylesheet_link_tag_defer "application"
= stylesheet_link_tag "disable_animations", media: "all" if Rails.env.test? || Gitlab.config.gitlab['disable_animations']
- = stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
+ = stylesheet_link_tag_defer 'performance_bar' if performance_bar_enabled?
- = stylesheet_link_tag "highlight/themes/#{user_color_scheme}", media: "all"
+ = stylesheet_link_tag_defer "highlight/themes/#{user_color_scheme}"
+
+ = render 'layouts/startup_css_activation'
= Gon::Base.render_data(nonce: content_security_policy_nonce)
@@ -70,6 +73,7 @@
= yield :page_specific_javascripts
= webpack_controller_bundle_tags
+ = webpack_bundle_tag "chrome_84_icon_fix" if browser.chrome?([">=84", "<85"]) || browser.edge?([">=84", "<85"])
= yield :project_javascripts
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 72b88fa8f7f..3a543fef292 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -15,6 +15,8 @@
= render "shared/ping_consent"
= render_account_recovery_regular_check
= render_if_exists "layouts/header/ee_subscribable_banner"
+ = render_if_exists "shared/namespace_storage_limit_alert"
+ = yield :customize_homepage_banner
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"
.d-flex
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 81fe0798bd1..0c6932e59a9 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -1,5 +1,5 @@
.search.search-form{ data: { track_label: "navbar_search", track_event: "activate_form_input", track_value: "" } }
- = form_tag search_path, method: :get, class: 'form-inline' do |f|
+ = form_tag search_path, method: :get, class: 'form-inline' do |_f|
.search-input-container
.search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } }
@@ -20,8 +20,8 @@
%a
= _('Loading...')
= dropdown_loading
- = sprite_icon('search', size: 16, css_class: 'search-icon')
- = sprite_icon('close', size: 16, css_class: 'clear-icon js-clear-input')
+ = sprite_icon('search', css_class: 'search-icon')
+ = sprite_icon('close', css_class: 'clear-icon js-clear-input')
= hidden_field_tag :group_id, search_context.for_group? ? search_context.group.id : '', class: 'js-search-group-options', data: search_context.group_metadata
= hidden_field_tag :project_id, search_context.for_project? ? search_context.project.id : '', id: 'search_project_id', class: 'js-search-project-options', data: search_context.project_metadata
diff --git a/app/views/layouts/_startup_css.haml b/app/views/layouts/_startup_css.haml
new file mode 100644
index 00000000000..094038d39b0
--- /dev/null
+++ b/app/views/layouts/_startup_css.haml
@@ -0,0 +1,4 @@
+- return unless use_startup_css?
+
+%style{ type: "text/css" }
+ = Rails.application.assets_manifest.find_sources('startup/startup-general.css').first.to_s.html_safe
diff --git a/app/views/layouts/_startup_css_activation.haml b/app/views/layouts/_startup_css_activation.haml
new file mode 100644
index 00000000000..0b1cce06f47
--- /dev/null
+++ b/app/views/layouts/_startup_css_activation.haml
@@ -0,0 +1,7 @@
+- return unless use_startup_css?
+
+= javascript_tag nonce: true do
+ :plain
+ document.querySelectorAll('link[media="print"]').forEach(linkTag => {
+ linkTag.addEventListener('load', function() {this.media='all'}, {once: true});
+ })
diff --git a/app/views/layouts/devise_experimental_onboarding_issues.html.haml b/app/views/layouts/devise_experimental_onboarding_issues.html.haml
new file mode 100644
index 00000000000..df2afbe60ae
--- /dev/null
+++ b/app/views/layouts/devise_experimental_onboarding_issues.html.haml
@@ -0,0 +1,11 @@
+!!! 5
+%html.devise-layout-html.navless{ class: system_message_class }
+ = render "layouts/head"
+ %body.ui-indigo.signup-page{ class: "#{client_class_list}", data: { page: body_data_page, qa_selector: 'signup_page' } }
+ = render "layouts/header/logo_with_title"
+ = render "layouts/init_client_detection_flags"
+ .page-wrap
+ .container.signup-box-container.navless-container
+ = render "layouts/broadcast"
+ .content
+ = yield
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 36b664e5888..8f4c89a9e77 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -3,6 +3,7 @@
- header_title group_title(@group) unless header_title
- nav "group"
- display_subscription_banner!
+- display_namespace_storage_limit_alert!
- @left_sidebar = true
- content_for :page_specific_javascripts do
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index b4e25956f16..56b70c463d0 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -7,7 +7,7 @@
.title-container
%h1.title
%span.gl-sr-only GitLab
- = link_to root_path, title: _('Dashboard'), id: 'logo' do
+ = link_to root_path, title: _('Dashboard'), id: 'logo', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation') do
= brand_header_logo
- logo_text = brand_header_logo_type
- if logo_text.present?
@@ -32,32 +32,47 @@
= render 'layouts/search' unless current_controller?(:search)
%li.nav-item.d-inline-block.d-lg-none
= link_to search_context.search_url, title: _('Search'), aria: { label: _('Search') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = sprite_icon('search', size: 16)
+ = sprite_icon('search')
- if header_link?(:issues)
= nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
- = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = sprite_icon('issues', size: 16)
+ = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') },
+ data: { qa_selector: 'issues_shortcut_button', toggle: 'tooltip', placement: 'bottom',
+ track_label: 'main_navigation',
+ track_event: 'click_issues_link',
+ track_property: 'navigation',
+ container: 'body' } do
+ = sprite_icon('issues')
- issues_count = assigned_issuables_count(:issues)
- %span.badge.badge-pill.issues-count.green-badge{ class: ('hidden' if issues_count.zero?) }
+ %span.badge.badge-pill.issues-count.green-badge{ class: ('hidden' if issues_count == 0) }
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
- = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _('Merge requests') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = sprite_icon('git-merge', size: 16)
+ = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _('Merge requests') },
+ data: { qa_selector: 'merge_requests_shortcut_button', toggle: 'tooltip', placement: 'bottom',
+ track_label: 'main_navigation',
+ track_event: 'click_merge_link',
+ track_property: 'navigation',
+ container: 'body' } do
+ = sprite_icon('git-merge')
- merge_requests_count = assigned_issuables_count(:merge_requests)
- %span.badge.badge-pill.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
+ %span.badge.badge-pill.merge-requests-count{ class: ('hidden' if merge_requests_count == 0) }
= number_with_delimiter(merge_requests_count)
- if header_link?(:todos)
= nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
- = link_to dashboard_todos_path, title: _('To-Do List'), aria: { label: _('To-Do List') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = sprite_icon('todo-done', size: 16)
- %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
+ = link_to dashboard_todos_path, title: _('To-Do List'), aria: { label: _('To-Do List') }, class: 'shortcuts-todos',
+ data: { qa_selector: 'todos_shortcut_button', toggle: 'tooltip', placement: 'bottom',
+ track_label: 'main_navigation',
+ track_event: 'click_to_do_link',
+ track_property: 'navigation',
+ container: 'body' } do
+ = sprite_icon('todo-done')
+ %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count == 0) }
= todos_count_format(todos_pending_count)
- %li.nav-item.header-help.dropdown.d-none.d-md-block
+ %li.nav-item.header-help.dropdown.d-none.d-md-block{ **tracking_attrs('main_navigation', 'click_question_mark_link', 'navigation') }
= link_to help_path, class: 'header-help-dropdown-toggle', data: { toggle: "dropdown" } do
%span.gl-sr-only
= s_('Nav|Help')
- = sprite_icon('question', size: 16)
+ = sprite_icon('question')
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/help_dropdown'
@@ -84,5 +99,8 @@
= sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
+- if ::Feature.enabled?(:whats_new_drawer)
+ #whats-new-app
+
- if can?(current_user, :update_user_status, current_user)
.js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } }
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 4bfac76ec5b..0c989242194 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,6 +1,6 @@
%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown", track_value: "" } }
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", id: "js-onboarding-new-project-link", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do
- = sprite_icon('plus-square', size: 16)
+ = sprite_icon('plus-square')
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
%ul
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index c344d3d484f..547d005a93e 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -2,11 +2,11 @@
- hide_top_links = @hide_top_links || false
%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] }
- .breadcrumbs-container{ class: ("border-bottom-0" if @no_breadcrumb_border && mr_tabs_position_enabled?) }
+ .breadcrumbs-container{ class: ("border-bottom-0" if @no_breadcrumb_border) }
- if defined?(@left_sidebar)
= button_tag class: 'toggle-mobile-nav', type: 'button' do
%span.sr-only= _("Open sidebar")
- = icon ('bars')
+ = sprite_icon('hamburger')
.breadcrumbs-links.js-title-container{ data: { qa_selector: 'breadcrumb_links_content' } }
%ul.list-unstyled.breadcrumbs-list.js-breadcrumbs-list
- unless hide_top_links
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index e6cfd7d56bb..29cacbe4aff 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -18,7 +18,7 @@
= render "layouts/nav/groups_dropdown/show"
- if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets])
- %li.header-more.dropdown
+ %li.header-more.dropdown{ **tracking_attrs('main_navigation', 'click_more_link', 'navigation') }
%a{ href: "#", data: { toggle: "dropdown", qa_selector: 'more_dropdown' } }
= _('More')
= sprite_icon('angle-down', css_class: 'caret-down')
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index e72535b8824..7fb5fff1e05 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -223,7 +223,7 @@
%span.nav-item-name.qa-admin-settings-item
= _('Settings')
- %ul.sidebar-sub-level-items.qa-admin-sidebar-settings-submenu
+ %ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_settings_submenu_content' } }
= nav_link(controller: [:application_settings, :integrations], html_options: { class: "fly-out-top-item" } ) do
= link_to general_admin_application_settings_path do
%strong.fly-out-top-item-name
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 909d72edb31..47dad21edd7 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -1,7 +1,7 @@
- issues_count = group_issues_count(state: 'opened')
- merge_requests_count = group_merge_requests_count(state: 'opened')
-.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **tracking_attrs('groups_side_navigation', 'render', 'groups_side_navigation') }
.nav-sidebar-inner-scroll
.context-header
= link_to group_path(@group), title: @group.name do
@@ -85,7 +85,7 @@
%span
= _('Milestones')
- = render_if_exists 'layouts/nav/sidebar/iterations_link'
+ = render_if_exists 'layouts/nav/sidebar/group_iterations_link'
- if group_sidebar_link?(:merge_requests)
= nav_link(path: 'groups#merge_requests') do
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 95d66786984..dadab554c02 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -1,4 +1,4 @@
-.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **tracking_attrs('user_side_navigation', 'render', 'user_side_navigation') }
.nav-sidebar-inner-scroll
.context-header
= link_to profile_path, title: _('Profile Settings') do
@@ -18,7 +18,7 @@
%strong.fly-out-top-item-name
= _('Profile')
= nav_link(controller: [:accounts, :two_factor_auths]) do
- = link_to profile_account_path do
+ = link_to profile_account_path, data: { qa_selector: 'profile_account_link' } do
.nav-icon-container
= sprite_icon('account')
%span.nav-item-name
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index d59c75de6d2..054311214ab 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -1,6 +1,5 @@
-.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
+.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **tracking_attrs('projects_side_navigation', 'render', 'projects_side_navigation') }
.nav-sidebar-inner-scroll
- - can_edit = can?(current_user, :admin_project, @project)
.context-header
= link_to project_path(@project), title: @project.name do
.avatar-container.rect-avatar.s40.project-avatar
@@ -121,6 +120,8 @@
%span
= _('Milestones')
+ = render_if_exists 'layouts/nav/sidebar/project_iterations_link'
+
- if project_nav_tab?(:external_issue_tracker)
- issue_tracker = @project.external_issue_tracker
- if issue_tracker.is_a?(JiraService) && project_jira_issues_integration?
@@ -221,17 +222,23 @@
%li.divider.fly-out-top-item
- if project_nav_tab? :metrics_dashboards
- = nav_link(controller: :environments, action: [:metrics, :metrics_redirect]) do
- = link_to metrics_project_environments_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
+ = nav_link(controller: :metrics_dashboard, action: [:show]) do
+ = link_to project_metrics_dashboard_path(@project), title: _('Metrics'), class: 'shortcuts-metrics', data: { qa_selector: 'operations_metrics_link' } do
%span
= _('Metrics')
- if project_nav_tab?(:alert_management)
= nav_link(controller: :alert_management) do
- = link_to project_alert_management_index_path(@project), title: _('Alerts'), class: 'shortcuts-tracking qa-operations-tracking-link' do
+ = link_to project_alert_management_index_path(@project), title: _('Alerts') do
%span
= _('Alerts')
+ - if project_nav_tab?(:incidents)
+ = nav_link(controller: :incidents) do
+ = link_to project_incidents_path(@project), title: _('Incidents'), data: { qa_selector: 'operations_incidents_link' } do
+ %span
+ = _('Incidents')
+
- if project_nav_tab? :environments
= render_if_exists "layouts/nav/sidebar/tracing_link"
@@ -242,10 +249,16 @@
- if project_nav_tab?(:error_tracking)
= nav_link(controller: :error_tracking) do
- = link_to project_error_tracking_index_path(@project), title: _('Error Tracking'), class: 'shortcuts-tracking qa-operations-tracking-link' do
+ = link_to project_error_tracking_index_path(@project), title: _('Error Tracking') do
%span
= _('Error Tracking')
+ - if project_nav_tab?(:product_analytics)
+ = nav_link(controller: :product_analytics) do
+ = link_to project_product_analytics_path(@project), title: _('Product Analytics') do
+ %span
+ = _('Product Analytics')
+
- if project_nav_tab? :serverless
= nav_link(controller: :functions) do
= link_to project_serverless_functions_path(@project), title: _('Serverless') do
diff --git a/app/views/layouts/nav/sidebar/_project_packages_link.html.haml b/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
index 0931ccdf637..e9989abe5a0 100644
--- a/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
+++ b/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
@@ -1,16 +1,23 @@
-- if project_nav_tab? :container_registry
- = nav_link controller: :repositories do
- = link_to project_container_registry_index_path(@project) do
+- packages_link = project_nav_tab?(:packages) ? project_packages_path(@project) : project_container_registry_index_path(@project)
+
+- if (project_nav_tab?(:packages) || project_nav_tab?(:container_registry))
+ = nav_link controller: [:packages, :repositories] do
+ = link_to packages_link, data: { qa_selector: 'packages_link' } do
.nav-icon-container
= sprite_icon('package')
%span.nav-item-name
= _('Packages & Registries')
%ul.sidebar-sub-level-items
- = nav_link(controller: :repositories, html_options: { class: "fly-out-top-item" } ) do
- = link_to project_container_registry_index_path(@project) do
+ = nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do
+ = link_to packages_link do
%strong.fly-out-top-item-name
= _('Packages & Registries')
%li.divider.fly-out-top-item
- = nav_link controller: :repositories do
- = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry', title: _('Container Registry') do
- %span= _('Container Registry')
+ - if project_nav_tab? :packages
+ = nav_link controller: :packages do
+ = link_to project_packages_path(@project), title: _('Package Registry') do
+ %span= _('Package Registry')
+ - if project_nav_tab? :container_registry
+ = nav_link controller: :repositories do
+ = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry', title: _('Container Registry') do
+ %span= _('Container Registry')
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 820cb9eea47..222ca02b1df 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -3,6 +3,7 @@
- header_title project_title(@project) unless header_title
- nav "project"
- display_subscription_banner!
+- display_namespace_storage_limit_alert!
- @left_sidebar = true
- content_for :project_javascripts do
diff --git a/app/views/notify/_relabeled_issuable_email.text.erb b/app/views/notify/_relabeled_issuable_email.text.erb
index 6a83d79fd61..dc399ef548d 100644
--- a/app/views/notify/_relabeled_issuable_email.text.erb
+++ b/app/views/notify/_relabeled_issuable_email.text.erb
@@ -1,3 +1,3 @@
<%= 'Label'.pluralize(@label_names.size) %> added: <%= @label_names.to_sentence %>
-<%= url_for([issuable.project.namespace.becomes(Namespace), issuable.project, issuable, { only_path: false }]) %>
+<%= url_for([issuable.project, issuable, { only_path: false }]) %>
diff --git a/app/views/notify/access_token_about_to_expire_email.html.haml b/app/views/notify/access_token_about_to_expire_email.html.haml
index d1923e324f7..240c7300c7f 100644
--- a/app/views/notify/access_token_about_to_expire_email.html.haml
+++ b/app/views/notify/access_token_about_to_expire_email.html.haml
@@ -4,4 +4,4 @@
= _('One or more of your personal access tokens will expire in %{days_to_expire} days or less.') % { days_to_expire: @days_to_expire }
%p
- pat_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
- = _('You can create a new one or check them in your %{pat_link_start}Personal Access Tokens%{pat_link_end} settings').html_safe % { pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe }
+ = html_escape(_('You can create a new one or check them in your %{pat_link_start}personal access tokens%{pat_link_end} settings')) % { pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe }
diff --git a/app/views/notify/access_token_about_to_expire_email.text.erb b/app/views/notify/access_token_about_to_expire_email.text.erb
index 5e6bd68d33f..edcec51aeb4 100644
--- a/app/views/notify/access_token_about_to_expire_email.text.erb
+++ b/app/views/notify/access_token_about_to_expire_email.text.erb
@@ -2,4 +2,4 @@
<%= _('One or more of your personal access tokens will expire in %{days_to_expire} days or less.') % { days_to_expire: @days_to_expire} %>
-<%= _('You can create a new one or check them in your Personal Access Tokens settings %{pat_link}') % { pat_link: @target_url } %>
+<%= _('You can create a new one or check them in your personal access tokens settings %{pat_link}') % { pat_link: @target_url } %>
diff --git a/app/views/notify/access_token_expired_email.html.haml b/app/views/notify/access_token_expired_email.html.haml
new file mode 100644
index 00000000000..b26431cce91
--- /dev/null
+++ b/app/views/notify/access_token_expired_email.html.haml
@@ -0,0 +1,7 @@
+%p
+ = _('Hi %{username}!') % { username: sanitize_name(@user.name) }
+%p
+ = _('One or more of your personal access tokens has expired.')
+%p
+ - pat_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
+ = html_escape(_('You can create a new one or check them in your %{pat_link_start}personal access tokens%{pat_link_end} settings')) % { pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe }
diff --git a/app/views/notify/access_token_expired_email.text.erb b/app/views/notify/access_token_expired_email.text.erb
new file mode 100644
index 00000000000..d44f993d094
--- /dev/null
+++ b/app/views/notify/access_token_expired_email.text.erb
@@ -0,0 +1,5 @@
+<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
+
+<%= _('One or more of your personal access tokens has expired.') %>
+
+<%= _('You can create a new one or check them in your personal access tokens settings %{pat_link}') % { pat_link: @target_url } %>
diff --git a/app/views/notify/reassigned_issue_email.text.erb b/app/views/notify/reassigned_issue_email.text.erb
index 7bf2e8e6ce3..dc0d8fc80b0 100644
--- a/app/views/notify/reassigned_issue_email.text.erb
+++ b/app/views/notify/reassigned_issue_email.text.erb
@@ -1,6 +1,6 @@
Reassigned Issue <%= @issue.iid %>
-<%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %>
+<%= url_for([@issue.project, @issue, { only_path: false }]) %>
Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%>
to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %>
diff --git a/app/views/notify/reassigned_merge_request_email.text.erb b/app/views/notify/reassigned_merge_request_email.text.erb
index 82ec7aa0fa4..2b51f48db3a 100644
--- a/app/views/notify/reassigned_merge_request_email.text.erb
+++ b/app/views/notify/reassigned_merge_request_email.text.erb
@@ -1,6 +1,6 @@
Reassigned Merge Request <%= @merge_request.iid %>
-<%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %>
+<%= url_for([@merge_request.project, @merge_request, { only_path: false }]) %>
Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%>
to <%= "#{@merge_request.assignees.any? ? @merge_request.assignee_list : 'Unassigned'}" %>
diff --git a/app/views/notify/service_desk_new_note_email.html.haml b/app/views/notify/service_desk_new_note_email.html.haml
index 7c6be6688d0..824b4ab712e 100644
--- a/app/views/notify/service_desk_new_note_email.html.haml
+++ b/app/views/notify/service_desk_new_note_email.html.haml
@@ -1,5 +1,5 @@
- if Gitlab::CurrentSettings.email_author_in_body
%div
- #{link_to @note.author_name, user_url(@note.author)} wrote:
+ = _("%{author_link} wrote:").html_safe % { author_link: link_to(@note.author_name, user_url(@note.author)) }
%div
= markdown(@note.note, pipeline: :email, author: @note.author)
diff --git a/app/views/notify/service_desk_new_note_email.text.erb b/app/views/notify/service_desk_new_note_email.text.erb
index 208953a437d..79144fc1bf4 100644
--- a/app/views/notify/service_desk_new_note_email.text.erb
+++ b/app/views/notify/service_desk_new_note_email.text.erb
@@ -1,6 +1,6 @@
-New response for issue #<%= @issue.iid %>:
+<%= _("New response for issue #%{issue_iid}:") % { issue_iid: @issue.iid } %>
-Author: <%= sanitize_name(@note.author_name) %>
+<%= _("Author: %{author_name}") % { author_name: sanitize_name(@note.author_name) } %>
<%= @note.note %>
<%# EE-specific start %><%= render_if_exists 'layouts/mailer/additional_text'%><%# EE-specific end %>
diff --git a/app/views/notify/service_desk_thank_you_email.html.haml b/app/views/notify/service_desk_thank_you_email.html.haml
index a3407acd9ba..ee61db40f07 100644
--- a/app/views/notify/service_desk_thank_you_email.html.haml
+++ b/app/views/notify/service_desk_thank_you_email.html.haml
@@ -1,2 +1,2 @@
%p
- Thank you for your support request! We are tracking your request as ticket ##{@issue.iid}, and will respond as soon as we can.
+ = _("Thank you for your support request! We are tracking your request as ticket #%{issue_iid}, and will respond as soon as we can.") % { issue_iid: @issue.iid }
diff --git a/app/views/notify/service_desk_thank_you_email.text.erb b/app/views/notify/service_desk_thank_you_email.text.erb
index 8281607a4a8..8b52219c83b 100644
--- a/app/views/notify/service_desk_thank_you_email.text.erb
+++ b/app/views/notify/service_desk_thank_you_email.text.erb
@@ -1,6 +1,6 @@
-Thank you for your support request! We are tracking your request as ticket #<%= @issue.iid %>, and will respond as soon as we can.
+<%= _("Thank you for your support request! We are tracking your request as ticket #%{issue_iid}, and will respond as soon as we can.") % { issue_iid: @issue.iid } %>
-To unsubscribe from this issue, please paste the following link into your browser:
+<%= _("To unsubscribe from this issue, please paste the following link into your browser:") %>
<%= @unsubscribe_url %>
<%# EE-specific start %><%= render_if_exists 'layouts/mailer/additional_text' %><%# EE-specific end %>
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index ea2f888c129..20660e61f38 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -17,7 +17,7 @@
- if current_user.two_factor_enabled?
= link_to _('Manage two-factor authentication'), profile_two_factor_auth_path, class: 'btn btn-info'
- else
- .append-bottom-10
+ .gl-mb-3
= link_to _('Enable two-factor authentication'), profile_two_factor_auth_path, class: 'btn btn-success'
%hr
@@ -56,7 +56,7 @@
= render 'users/deletion_guidance', user: current_user
%button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal',
- target: '#delete-account-modal' } }
+ target: '#delete-account-modal', qa_selector: 'delete_account_button' } }
= s_('Profiles|Delete account')
#delete-account-modal{ data: { action_url: user_registration_path,
diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml
index 5bed9e0d771..2134ab2bec6 100644
--- a/app/views/profiles/chat_names/new.html.haml
+++ b/app/views/profiles/chat_names/new.html.haml
@@ -1,15 +1,14 @@
-%h3.page-title Authorization required
+%h3.page-title
+ = _("Authorization required")
%main{ :role => "main" }
%p.h4
- Authorize
- %strong.text-info= @chat_name_params[:chat_name]
- to use your account?
+ = html_escape(_("Authorize %{user} to use your account?")) % { user: tag.strong(@chat_name_params[:chat_name]) }
%hr
.actions
= form_tag profile_chat_names_path, method: :post do
= hidden_field_tag :token, @chat_name_token.token
- = submit_tag "Authorize", class: "btn btn-success wide float-left"
+ = submit_tag _("Authorize"), class: "btn btn-success wide float-left"
= form_tag deny_profile_chat_names_path, method: :delete do
= hidden_field_tag :token, @chat_name_token.token
- = submit_tag "Deny", class: "btn btn-danger gl-ml-3"
+ = submit_tag _("Deny"), class: "btn btn-danger gl-ml-3"
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index fa7ab0666cc..a04ed87801a 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -60,4 +60,4 @@
= link_to profile_email_path(email), data: { confirm: _('Are you sure?'), qa_selector: 'delete_email_link'}, method: :delete, class: 'btn btn-sm btn-danger gl-ml-3' do
%span.sr-only= _('Remove')
- = icon('trash')
+ = sprite_icon('remove')
diff --git a/app/views/profiles/gpg_keys/_key.html.haml b/app/views/profiles/gpg_keys/_key.html.haml
index 7bbb0235cd8..e05f121c5d9 100644
--- a/app/views/profiles/gpg_keys/_key.html.haml
+++ b/app/views/profiles/gpg_keys/_key.html.haml
@@ -1,6 +1,6 @@
%li.key-list-item
.float-left.gl-mr-3
- = icon 'key', class: "settings-list-icon d-none d-sm-block"
+ = sprite_icon('key', css_class: "settings-list-icon d-none d-sm-block gl-mt-4")
.key-list-item-info
- key.emails_with_verified_status.map do |email, verified|
= render partial: 'shared/email_with_badge', locals: { email: email, verified: verified }
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index c9ab7b6fbd3..02b45853aa0 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -3,12 +3,12 @@
- if key.valid?
- if key.expired?
%span.d-inline-block.has-tooltip{ title: s_('Profiles|Your key has expired') }
- = sprite_icon('warning-solid', size: 16, css_class: 'settings-list-icon d-none d-sm-block')
+ = sprite_icon('warning-solid', css_class: 'settings-list-icon d-none d-sm-block')
- else
- = sprite_icon('key', size: 16, css_class: 'settings-list-icon d-none d-sm-block ')
+ = sprite_icon('key', css_class: 'settings-list-icon d-none d-sm-block ')
- else
%span.d-inline-block.has-tooltip{ title: key.errors.full_messages.join(', ') }
- = sprite_icon('warning-solid', size: 16, css_class: 'settings-list-icon d-none d-sm-block')
+ = sprite_icon('warning-solid', css_class: 'settings-list-icon d-none d-sm-block')
.key-list-item-info.w-100.float-none
= link_to path_to_key(key, is_admin), class: "title" do
@@ -28,4 +28,4 @@
- if key.can_delete?
= link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent gl-ml-3 align-baseline" do
%span.sr-only= _('Remove')
- = sprite_icon('remove', size: 16)
+ = sprite_icon('remove')
diff --git a/app/views/profiles/preferences/_sourcegraph.html.haml b/app/views/profiles/preferences/_sourcegraph.html.haml
index 7328d36b0fb..f3530da9a5f 100644
--- a/app/views/profiles/preferences/_sourcegraph.html.haml
+++ b/app/views/profiles/preferences/_sourcegraph.html.haml
@@ -4,7 +4,7 @@
.col-sm-12
%hr
-.col-lg-4.profile-settings-sidebar
+.col-lg-4.profile-settings-sidebar#integrations
%h4.gl-mt-0
= s_('Preferences|Integrations')
%p
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index bc1f2cb3072..54ca8788864 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -2,7 +2,7 @@
- @content_class = "limit-container-width" unless fluid_layout
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row gl-mt-3 js-preferences-form' } do |f|
- .col-lg-4.application-theme
+ .col-lg-4.application-theme#navigation-theme
%h4.gl-mt-0
= s_('Preferences|Navigation theme')
%p
@@ -18,7 +18,7 @@
.col-sm-12
%hr
- .col-lg-4.profile-settings-sidebar
+ .col-lg-4.profile-settings-sidebar#syntax-highlighting-theme
%h4.gl-mt-0
= s_('Preferences|Syntax highlighting theme')
%p
@@ -35,7 +35,7 @@
.col-sm-12
%hr
- .col-lg-4.profile-settings-sidebar
+ .col-lg-4.profile-settings-sidebar#behavior
%h4.gl-mt-0
= s_('Preferences|Behavior')
%p
@@ -51,8 +51,10 @@
= s_('Preferences|Choose between fixed (max. 1280px) and fluid (%{percentage}) application layout.').html_safe % { percentage: '100%' }
.form-group
= f.label :dashboard, class: 'label-bold' do
- = s_('Preferences|Default dashboard')
+ = s_('Preferences|Homepage content')
= f.select :dashboard, dashboard_choices, {}, class: 'select2'
+ .form-text.text-muted
+ = s_('Preferences|Choose what content you want to see on your homepage.')
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
@@ -90,7 +92,7 @@
.col-sm-12
%hr
- .col-lg-4.profile-settings-sidebar
+ .col-lg-4.profile-settings-sidebar#localization
%h4.gl-mt-0
= _('Localization')
%p
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index f4aa0b98e37..672f9c9a0c0 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -29,7 +29,7 @@
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon_for_user(@user, 160), alt: '', class: 'avatar s160'
%h5.gl-mt-0= s_("Profiles|Upload new avatar")
- .gl-mt-2.append-bottom-10
+ .gl-mt-2.gl-mb-3
%button.btn.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")
= f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml
index 68cd4875a33..40272b6354c 100644
--- a/app/views/profiles/two_factor_auths/_codes.html.haml
+++ b/app/views/profiles/two_factor_auths/_codes.html.haml
@@ -2,11 +2,11 @@
- lose_2fa_message = _('Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account.') % { b_start:'<b>', b_end:'</b>' }
= lose_2fa_message.html_safe
-.codes.card
+.codes.card{ data: { qa_selector: 'codes_content' } }
%ul
- @codes.each do |code|
%li
- %span.monospace= code
+ %span.monospace{ data: { qa_selector: 'code_content' } }= code
.d-flex
= link_to _('Proceed'), profile_account_path, class: 'btn btn-success gl-mr-3', data: { qa_selector: 'proceed_button' }
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 0fde3e5fb10..bce43b16d27 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -28,7 +28,7 @@
- help_link_start = '<a href="%{url}" target="_blank">' % { url: help_page_path('user/profile/account/two_factor_authentication') }
- register_2fa_token = _('Install a soft token authenticator like %{free_otp_link} or Google Authenticator from your application repository and use that app to scan this QR code. More information is available in the %{help_link_start}documentation%{help_link_end}.') % { free_otp_link:'<a href="https://freeotp.github.io/">FreeOTP</a>', help_link_start:help_link_start, help_link_end:'</a>' }
= register_2fa_token.html_safe
- .row.append-bottom-10
+ .row.gl-mb-3
.col-md-4
= raw @qr_code
.col-md-8
@@ -88,7 +88,7 @@
%tbody
- @u2f_registrations.each do |registration|
%tr
- %td= registration.name.presence || _("<no name set>")
+ %td= registration.name.presence || html_escape_once(_("&lt;no name set&gt;")).html_safe
%td= registration.created_at.to_date.to_s(:medium)
%td= link_to _('Delete'), profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger float-right", data: { confirm: _('Are you sure you want to delete this device? This action cannot be undone.') }
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 07faf5a66da..c47ca81c431 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,9 +1,14 @@
+- is_project_overview = local_assigns.fetch(:is_project_overview, false)
+
%div{ class: container_class }
- .nav-block.d-none.d-sm-flex.activities
+ .nav-block.d-none.d-sm-flex.activities.gl-static
= render 'shared/event_filter'
- .controls
- = link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn d-none d-sm-inline-block has-tooltip' do
- = icon('rss')
+ .controls.gl-display-flex
+ = link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip' do
+ = sprite_icon('rss', css_class: 'qa-rss-icon gl-icon')
+ - if is_project_overview && can?(current_user, :download_code, @project)
+ .project-clone-holder.d-none.d-md-inline-flex.gl-ml-2
+ = render "projects/buttons/clone", dropdown_class: 'dropdown-menu-right'
.content_list.project-activity{ :"data-href" => activity_project_path(@project) }
.loading
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 7da15e0d8a5..41e13464b1e 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -26,5 +26,6 @@
= link_to _('Generate new export'), generate_new_export_project_path(project),
method: :post, class: "btn btn-default"
- else
- = link_to _('Export project'), export_project_path(project),
+ .gl-display-flex.gl-justify-content-end
+ = link_to _('Export project'), export_project_path(project),
method: :post, class: "btn btn-default", data: { qa_selector: 'export_project_link' }
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index ab8275ba5e4..f9222387e97 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -9,4 +9,3 @@
= render 'shared/auto_devops_implicitly_enabled_banner', project: project
= render_if_exists 'projects/above_size_limit_warning', project: project
= render_if_exists 'shared/shared_runners_minutes_limit', project: project, classes: [container_class, ("limit-container-width" unless fluid_layout)]
- = render_if_exists 'shared/namespace_storage_limit_alert', namespace: project.namespace, classes: [container_class, ("limit-container-width" unless fluid_layout)]
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 9966baf78f4..94a2bdb3bcb 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -13,7 +13,7 @@
%h1.home-panel-title.gl-mt-3.gl-mb-2{ data: { qa_selector: 'project_name_content' } }
= @project.name
%span.visibility-icon.text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
- = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'})
+ = visibility_level_icon(@project.visibility_level, options: { class: 'icon' })
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project
.home-panel-metadata.d-flex.flex-wrap.text-secondary
- if can?(current_user, :read_project, @project)
@@ -24,7 +24,7 @@
= render 'shared/members/access_request_links', source: @project
- if @project.tag_list.present?
%span.home-panel-topic-list.mt-2.w-100.d-inline-flex
- = sprite_icon('tag', size: 16, css_class: 'icon gl-mr-2')
+ = sprite_icon('tag', css_class: 'icon gl-mr-2')
- @project.topics_to_show.each do |topic|
- project_topics_classes = "badge badge-pill badge-secondary gl-mr-2"
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index bb278fbf311..dd7971f6db0 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -46,7 +46,7 @@
- if fogbugz_import_enabled?
%div
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz', **tracking_attrs(track_label, 'click_button', 'fogbugz') do
- = icon('bug', text: 'Fogbugz')
+ = icon('bug', text: 'FogBugz')
- if gitea_import_enabled?
%div
@@ -56,8 +56,9 @@
- if git_import_enabled?
%div
- %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' }, **tracking_attrs(track_label, 'click_button', 'repo_url') }
- = icon('git', text: 'Repo by URL')
+ %button.btn.btn-svg.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' }, **tracking_attrs(track_label, 'click_button', 'repo_url') }
+ = sprite_icon('link', css_class: 'gl-icon')
+ = _('Repo by URL')
- if manifest_import_enabled?
%div
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index 5ffdeef3558..e69972e8163 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -4,7 +4,7 @@
= render 'projects/merge_request_merge_options_settings', project: @project, form: form
-- if Feature.enabled?(:squash_options, @project)
+- if Feature.enabled?(:squash_options, @project, default_enabled: true)
= render 'projects/merge_request_squash_options_settings', form: form
= render 'projects/merge_request_merge_checks_settings', project: @project, form: form
diff --git a/app/views/projects/_remove.html.haml b/app/views/projects/_remove.html.haml
index 528d802261c..05eab3b3245 100644
--- a/app/views/projects/_remove.html.haml
+++ b/app/views/projects/_remove.html.haml
@@ -1,9 +1,10 @@
- return unless can?(current_user, :remove_project, project)
+- confirm_phrase = s_('DeleteProject|Delete %{name}') % { name: project.full_name }
.sub-section
- %h4.danger-title= _('Remove project')
+ %h4.danger-title= _('Delete project')
%p
- %strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
+ %strong= _('Deleting the project will delete its repository and all related resources including issues, merge requests etc.')
%p
- %strong= _('Removed projects cannot be restored!')
- #js-confirm-project-remove{ data: { form_path: project_path(project), confirm_phrase: project.path, warning_message: remove_project_message(project) } }
+ %strong= _('Deleted projects cannot be restored!')
+ #js-project-delete-button{ data: { form_path: project_path(project), confirm_phrase: confirm_phrase } }
diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml
index e6842bbb939..7c08955983a 100644
--- a/app/views/projects/_service_desk_settings.html.haml
+++ b/app/views/projects/_service_desk_settings.html.haml
@@ -10,7 +10,8 @@
- if ::Gitlab::ServiceDesk.supported?
.js-service-desk-setting-root{ data: { endpoint: project_service_desk_path(@project),
enabled: "#{@project.service_desk_enabled}",
- incoming_email: (@project.service_desk_address if @project.service_desk_enabled),
+ incoming_email: (@project.service_desk_incoming_address if @project.service_desk_enabled),
+ custom_email: (@project.service_desk_custom_address if @project.service_desk_enabled),
selected_template: "#{@project.service_desk_setting&.issue_template_key}",
outgoing_name: "#{@project.service_desk_setting&.outgoing_name}",
project_key: "#{@project.service_desk_setting&.project_key}",
diff --git a/app/views/projects/_visibility_modal.html.haml b/app/views/projects/_visibility_modal.html.haml
index 3ef93a40137..144f726572b 100644
--- a/app/views/projects/_visibility_modal.html.haml
+++ b/app/views/projects/_visibility_modal.html.haml
@@ -7,7 +7,7 @@
.modal-header
%h3.page-title= _('Reduce this project’s visibility?')
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": true }= sprite_icon("close", size: 16)
+ %span{ "aria-hidden": true }= sprite_icon("close")
.modal-body
%p
- if @project.group
@@ -23,8 +23,7 @@
= ("To confirm, type %{phrase_code}").html_safe % { phrase_code: '<code class="js-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: @project.full_path } }
.form-group
= text_field_tag 'confirm_path_input', '', class: 'form-control js-confirm-danger-input qa-confirm-input'
- .form-actions.clearfix
- .pull-right
- %button.btn.btn-default{ type: "button", "data-dismiss": "modal" }
- = _('Cancel')
- = submit_tag _('Reduce project visibility'), class: "btn btn-danger js-confirm-danger-submit qa-confirm-button", disabled: true
+ .form-actions.gl-display-flex.gl-justify-content-end
+ %button.btn.btn-default.gl-mr-4{ type: "button", "data-dismiss": "modal" }
+ = _('Cancel')
+ = submit_tag _('Reduce project visibility'), class: "btn btn-danger js-confirm-danger-submit qa-confirm-button", disabled: true
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index a2d6b2e18a9..2f3d0660caa 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,5 +1,5 @@
- page_title _("Blame"), @blob.path, @ref
-- link_icon = icon("link")
+- link_icon = sprite_icon("link", size: 12)
#blob-content-holder.tree-holder
= render "projects/blob/breadcrumb", blob: @blob, blame: true
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index b06ae31e73f..787dc3b030f 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -8,13 +8,13 @@
= sprite_icon('fork', size: 12)
= ref
- if current_action?(:edit) || current_action?(:update)
- %span.pull-left.gl-mr-3
+ %span.float-left.gl-mr-3
= text_field_tag 'file_path', (params[:file_path] || @path),
class: 'form-control new-file-path js-file-path-name-input'
= render 'template_selectors'
- if current_action?(:new) || current_action?(:create)
- %span.pull-left.gl-mr-3
+ %span.float-left.gl-mr-3
\/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name",
required: true, class: 'form-control new-file-name js-file-path-name-input', value: params[:file_name] || (should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : '')
@@ -40,7 +40,8 @@
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1'
.file-editor.code
- %pre.js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }= params[:content] || local_assigns[:blob_data]
+ .js-edit-mode-pane.qa-editor#editor{ data: { 'editor-loading': true } }<
+ %pre.editor-loading-content= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
.center
diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml
index 32adfb320ff..30356348941 100644
--- a/app/views/projects/blob/_header_content.html.haml
+++ b/app/views/projects/blob/_header_content.html.haml
@@ -7,6 +7,8 @@
= copy_file_path_button(blob.path)
%small.mr-1
+ - if blob.mode == Blob::MODE_SYMLINK
+ = _('Symbolic link') << ' ·'
= number_to_human_size(blob.raw_size)
- if blob.stored_externally? && blob.external_storage == :lfs
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
index ba8029ac32a..2aefcdc5762 100644
--- a/app/views/projects/blob/_template_selectors.html.haml
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -7,6 +7,8 @@
= 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 } } )
.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) } } )
+ .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) } } )
#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) } } )
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 1319c58eb38..7d072ba5899 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,8 +1,5 @@
- breadcrumb_title _("Repository")
- page_title _("Edit"), @blob.path, @ref
-- unless Feature.enabled?(:monaco_blobs)
- - content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/ace.js')
- if @conflict
.alert.alert-danger
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 2420c4a4bd5..48ffd80aa9c 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,8 +1,5 @@
- breadcrumb_title _("Repository")
- page_title _("New File"), @path.presence, @ref
-- unless Feature.enabled?(:monaco_blobs)
- - content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/ace.js')
.editor-title-row
%h3.page-title.blob-new-page-title
diff --git a/app/views/projects/blob/viewers/_changelog.html.haml b/app/views/projects/blob/viewers/_changelog.html.haml
index 46e3e7f798a..80ead53beff 100644
--- a/app/views/projects/blob/viewers/_changelog.html.haml
+++ b/app/views/projects/blob/viewers/_changelog.html.haml
@@ -1,4 +1,4 @@
-= icon('history fw')
+= sprite_icon('history', css_class: 'gl-mr-1 gl-vertical-align-text-bottom')
= succeed '.' do
To find the state of this project's repository at the time of any of these versions, check out
= link_to "the tags", project_tags_path(viewer.project)
diff --git a/app/views/projects/blob/viewers/_contributing.html.haml b/app/views/projects/blob/viewers/_contributing.html.haml
index 546c064c06f..18559e2908f 100644
--- a/app/views/projects/blob/viewers/_contributing.html.haml
+++ b/app/views/projects/blob/viewers/_contributing.html.haml
@@ -1,4 +1,4 @@
-= icon('book fw')
+= sprite_icon('book')
After you've reviewed these contribution guidelines, you'll be all set to
- options = contribution_options(viewer.project)
diff --git a/app/views/projects/blob/viewers/_dependency_manager.html.haml b/app/views/projects/blob/viewers/_dependency_manager.html.haml
index 5970d41fdab..e6de420bbb1 100644
--- a/app/views/projects/blob/viewers/_dependency_manager.html.haml
+++ b/app/views/projects/blob/viewers/_dependency_manager.html.haml
@@ -1,6 +1,5 @@
-= icon('cubes fw')
+= sprite_icon('package')
= succeed '.' do
- This project manages its dependencies using
- %strong= viewer.manager_name
+ = _("This project manages its dependencies using %{strong_start}%{manager_name}%{strong_end}").html_safe % { manager_name: viewer.manager_name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
-= link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer'
+= link_to _('Learn more'), viewer.manager_url, target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/blob/viewers/_license.html.haml b/app/views/projects/blob/viewers/_license.html.haml
index 7ac0e7bb579..d2bd90a898a 100644
--- a/app/views/projects/blob/viewers/_license.html.haml
+++ b/app/views/projects/blob/viewers/_license.html.haml
@@ -1,6 +1,6 @@
- license = viewer.license
-= sprite_icon('scale', size: 16)
+= sprite_icon('scale')
This project is licensed under the
= succeed '.' do
%strong= license.name
diff --git a/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml b/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml
index ecbf6d9005d..9ec1d7d0d67 100644
--- a/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml
+++ b/app/views/projects/blob/viewers/_metrics_dashboard_yml.html.haml
@@ -8,4 +8,4 @@
- viewer.errors.messages.each do |error|
%li= error.join(': ')
-= link_to _('Learn more'), help_page_path('operations/metrics/dashboards/index.md', anchor: 'defining-custom-dashboards-per-project')
+= link_to _('Learn more'), help_page_path('operations/metrics/dashboards/index.md')
diff --git a/app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml b/app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml
index 31a0d514444..aedfb64d3e4 100644
--- a/app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml
+++ b/app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml
@@ -1,4 +1,4 @@
= icon('spinner spin fw')
= _('Metrics Dashboard YAML definition') + '…'
-= link_to _('Learn more'), help_page_path('user/project/integrations/prometheus.md')
+= link_to _('Learn more'), help_page_path('operations/metrics/dashboards/yaml.md')
diff --git a/app/views/projects/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml
index 6cbd26e2271..86f59146cda 100644
--- a/app/views/projects/blob/viewers/_readme.html.haml
+++ b/app/views/projects/blob/viewers/_readme.html.haml
@@ -1,4 +1,4 @@
-= icon('info-circle fw')
+= sprite_icon('information-o', css_class: 'gl-vertical-align-middle! gl-mr-2')
= succeed '.' do
To learn more about this project, read
= link_to "the wiki", wiki_path(viewer.project.wiki)
diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml
index 828371e9656..f03b5cf2eff 100644
--- a/app/views/projects/branches/_panel.html.haml
+++ b/app/views/projects/branches/_panel.html.haml
@@ -7,7 +7,7 @@
- return unless branches.any?
-.card.prepend-top-10
+.card.gl-mt-3
.card-header
= panel_title
%ul.content-list.all-branches.qa-all-branches
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 33465953086..5effa5a9e92 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -8,9 +8,9 @@
- if show_menu
.project-action-button.dropdown.inline<
- %a.btn.dropdown-toggle.has-tooltip.qa-create-new-dropdown{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' }
- = icon('plus')
- = icon("caret-down")
+ %a.btn.btn-default.gl-button.dropdown-toggle.has-tooltip.qa-create-new-dropdown{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' }
+ = sprite_icon('plus', css_class: 'gl-icon')
+ = sprite_icon("chevron-down", css_class: 'gl-icon')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can_create_issue || merge_project || can_create_project_snippet
%li.dropdown-header= _('This project')
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 4c20ac84b24..23f9a6a8f6c 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -97,7 +97,7 @@
%td
.float-right
- if can?(current_user, :read_build, job) && job.artifacts?
- = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build' do
+ = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build gl-button btn-icon btn-svg' do
= sprite_icon('download')
- if can?(current_user, :update_build, job)
- if job.active?
@@ -126,5 +126,5 @@
= link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), class: 'btn btn-build' do
= custom_icon('icon_play')
- elsif job.retryable?
- = link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
- = icon('repeat')
+ = link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build gl-button btn-icon btn-default' do
+ = sprite_icon('repeat', css_class: 'gl-icon')
diff --git a/app/views/projects/ci/lints/_create.html.haml b/app/views/projects/ci/lints/_create.html.haml
index d65c06aa2a4..5cc89343ba3 100644
--- a/app/views/projects/ci/lints/_create.html.haml
+++ b/app/views/projects/ci/lints/_create.html.haml
@@ -4,6 +4,8 @@
%b= _("Status:")
= _("syntax is correct")
+ = render "projects/ci/lints/lint_warnings", warnings: @warnings
+
.table-holder
%table.table.table-bordered
%thead
@@ -11,33 +13,54 @@
%th= _("Parameter")
%th= _("Value")
%tbody
- - @stages.each do |stage|
- - @builds.select { |build| build[:stage] == stage }.each do |build|
- - job = @jobs[build[:name].to_sym]
- %tr
- %td #{stage.capitalize} Job - #{build[:name]}
- %td
- %pre= job[:before_script].to_a.join('\n')
- %pre= job[:script].to_a.join('\n')
- %pre= job[:after_script].to_a.join('\n')
+ - if @dry_run
+ - @stages.each do |stage|
+ - stage.statuses.each do |job|
+ %tr
+ %td #{stage.name.capitalize} Job - #{job.name}
+ %td
+ %pre= job.options[:before_script].to_a.join('\n')
+ %pre= job.options[:script].to_a.join('\n')
+ %pre= job.options[:after_script].to_a.join('\n')
+ %br
+ %b= _("Tag list:")
+ = job.tag_list.to_a.join(", ") if job.is_a?(Ci::Build)
+ %br
+ %b= _("Environment:")
+ = job.options.dig(:environment, :name)
+ %br
+ %b= _("When:")
+ = job.when
+ - if job.allow_failure
+ %b= _("Allowed to fail")
- %br
- %b= _("Tag list:")
- = build[:tag_list].to_a.join(", ")
- %br
- %b= _("Only policy:")
- = job[:only].to_a.join(", ")
- %br
- %b= _("Except policy:")
- = job[:except].to_a.join(", ")
- %br
- %b= _("Environment:")
- = build[:environment]
- %br
- %b= _("When:")
- = build[:when]
- - if build[:allow_failure]
- %b= _("Allowed to fail")
+ - else
+ - @stages.each do |stage|
+ - @builds.select { |build| build[:stage] == stage }.each do |build|
+ - job = @jobs[build[:name].to_sym]
+ %tr
+ %td #{stage.capitalize} Job - #{build[:name]}
+ %td
+ %pre= job[:before_script].to_a.join('\n')
+ %pre= job[:script].to_a.join('\n')
+ %pre= job[:after_script].to_a.join('\n')
+ %br
+ %b= _("Tag list:")
+ = build[:tag_list].to_a.join(", ")
+ %br
+ %b= _("Only policy:")
+ = job[:only].to_a.join(", ")
+ %br
+ %b= _("Except policy:")
+ = job[:except].to_a.join(", ")
+ %br
+ %b= _("Environment:")
+ = build[:environment]
+ %br
+ %b= _("When:")
+ = build[:when]
+ - if build[:allow_failure]
+ %b= _("Allowed to fail")
- else
.bs-callout.bs-callout-danger
@@ -47,3 +70,5 @@
%pre
- @errors.each do |message|
%p= message
+
+ = render "projects/ci/lints/lint_warnings", warnings: @warnings
diff --git a/app/views/projects/ci/lints/_lint_warnings.html.haml b/app/views/projects/ci/lints/_lint_warnings.html.haml
new file mode 100644
index 00000000000..0a5bb8f76ef
--- /dev/null
+++ b/app/views/projects/ci/lints/_lint_warnings.html.haml
@@ -0,0 +1,6 @@
+- if warnings
+ - warnings.each do |warning|
+ .bs-callout.bs-callout-warning
+ %p
+ %b= _("Warning:")
+ = markdown(warning)
diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml
index 7b87664961e..0c51c978bfe 100644
--- a/app/views/projects/ci/lints/show.html.haml
+++ b/app/views/projects/ci/lints/show.html.haml
@@ -3,7 +3,7 @@
- content_for :library_javascripts do
= page_specific_javascript_tag('lib/ace.js')
-%h2.pt-3.pb-3= _("Check your .gitlab-ci.yml")
+%h2.pt-3.pb-3= _("Validate your GitLab CI configuration")
.project-ci-linter
= form_tag project_ci_lint_path(@project), method: :post do
@@ -15,8 +15,12 @@
#ci-editor.ci-editor= @content
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
- .float-left.prepend-top-10
+ .float-left.gl-mt-3
= submit_tag(_('Validate'), class: 'btn btn-success submit-yml')
+ - if Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project)
+ = check_box_tag(:dry_run, 'true', params[:dry_run])
+ = label_tag(:dry_run, _('Simulate a pipeline created for the default branch'))
+ = link_to icon('question-circle'), help_page_path('ci/lint', anchor: 'pipeline-simulation'), target: '_blank', rel: 'noopener noreferrer'
.float-right.prepend-top-10
= button_tag(_('Clear'), type: 'button', class: 'btn btn-default clear-yml')
diff --git a/app/views/projects/cleanup/_show.html.haml b/app/views/projects/cleanup/_show.html.haml
index 52855d7ee12..f560127fefd 100644
--- a/app/views/projects/cleanup/_show.html.haml
+++ b/app/views/projects/cleanup/_show.html.haml
@@ -14,8 +14,8 @@
.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|
- %fieldset.gl-mt-0.append-bottom-10
- .append-bottom-10
+ %fieldset.gl-mt-0.gl-mb-3
+ .gl-mb-3
%h5.gl-mt-0
= _("Upload object map")
%button.btn.btn-default.js-choose-file{ type: "button" }
@@ -25,5 +25,7 @@
= f.file_field :bfg_object_map, class: "hidden js-object-map-input", required: true
.form-text.text-muted
= _("The maximum file size allowed is %{size}.") % { size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) }
- = f.submit _('Start cleanup'), class: 'btn btn-success'
+
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Start cleanup'), class: 'btn btn-success'
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index e71615dd1c5..4f5d69c614c 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -21,7 +21,7 @@
.modal-body
- if description
%p= description
- = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do
+ = form_tag [type.underscore, @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do
.form-group.branch
= label_tag 'start_branch', branch_label, class: 'label-bold'
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 71cf6ca6922..29ee4a69e83 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -93,7 +93,7 @@
- if @merge_request
.well-segment
- = icon('info-circle fw')
+ = sprite_icon('information-o', css_class: 'gl-vertical-align-middle! gl-mr-2')
- link_to_merge_request = link_to(@merge_request.to_reference, diffs_project_merge_request_path(@project, @merge_request, commit_id: @commit.id))
= _('This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request.').html_safe % { link_to_merge_request: link_to_merge_request }
diff --git a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
index d282ab4f520..e56579b162f 100644
--- a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
+++ b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
@@ -1,5 +1,5 @@
- title = capture do
- = _('This commit was signed with a verified signature, but the committer email is <strong>not verified</strong> to belong to the same user.').html_safe
+ = html_escape(_('This commit was signed with a verified signature, but the committer email is %{strong_open}not verified%{strong_close} to belong to the same user.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
- locals = { signature: signature, title: title, label: _('Unverified'), css_class: ['invalid'], icon: 'status_notfound_borderless', show_user: true }
diff --git a/app/views/projects/commit/_unverified_signature_badge.html.haml b/app/views/projects/commit/_unverified_signature_badge.html.haml
index 294f916d18f..0ce8e06382b 100644
--- a/app/views/projects/commit/_unverified_signature_badge.html.haml
+++ b/app/views/projects/commit/_unverified_signature_badge.html.haml
@@ -1,5 +1,5 @@
- title = capture do
- = _('This commit was signed with an <strong>unverified</strong> signature.').html_safe
+ = html_escape(_('This commit was signed with an %{strong_open}unverified%{strong_close} signature.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless' }
diff --git a/app/views/projects/commit/diff_files.html.haml b/app/views/projects/commit/diff_files.html.haml
new file mode 100644
index 00000000000..3a473be3840
--- /dev/null
+++ b/app/views/projects/commit/diff_files.html.haml
@@ -0,0 +1,3 @@
+- diff_files = diffs.diff_files
+
+= render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: 'is-commit' }
diff --git a/app/views/projects/commit/x509/_verified_signature_badge.html.haml b/app/views/projects/commit/x509/_verified_signature_badge.html.haml
index 4964b1b8ee7..357ad467539 100644
--- a/app/views/projects/commit/x509/_verified_signature_badge.html.haml
+++ b/app/views/projects/commit/x509/_verified_signature_badge.html.haml
@@ -1,5 +1,5 @@
- title = capture do
- = _('This commit was signed with a <strong>verified</strong> signature and the committer email is verified to belong to the same user.').html_safe
+ = html_escape(_('This commit was signed with a %{strong_open}verified%{strong_close} signature and the committer email is verified to belong to the same user.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
- locals = { signature: signature, title: title, label: _('Verified'), css_class: 'valid', icon: 'status_success_borderless', show_user: true }
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index ab1d855a6e0..33fedde0cd1 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -10,12 +10,13 @@
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- commit = commit.present(current_user: current_user)
- commit_status = commit.status_for(ref)
+- collapsible = local_assigns.fetch(:collapsible, true)
- link = commit_path(project, commit, merge_request: merge_request)
- show_project_name = local_assigns.fetch(:show_project_name, false)
-%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
+%li{ class: ["commit flex-row", ("js-toggle-container" if collapsible)], id: "commit-#{commit.short_id}" }
.avatar-cell.d-none.d-sm-block
= author_avatar(commit, size: 40, has_tooltip: false)
@@ -29,7 +30,7 @@
%span.commit-row-message.d-inline.d-sm-none
&middot;
= commit.short_id
- - if commit.description?
+ - if commit.description? && collapsible
%button.text-expander.js-toggle-button
= sprite_icon('ellipsis_h', size: 12)
@@ -41,7 +42,7 @@
= render_if_exists 'projects/commits/project_namespace', show_project_name: show_project_name, project: project
- if commit.description?
- %pre.commit-row-description.js-toggle-content.gl-mb-3
+ %pre{ class: ["commit-row-description gl-mb-3", (collapsible ? "js-toggle-content" : "d-block")] }
= preserve(markdown_field(commit, :description))
.commit-actions.flex-row
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index e413bd78789..293500a6c31 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -1,8 +1,10 @@
- merge_request = local_assigns.fetch(:merge_request, nil)
- project = local_assigns.fetch(:project) { merge_request&.project }
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
+- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- commits = @commits
+- context_commits = @context_commits
- hidden = @hidden_commit_count
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, daily_commits|
@@ -14,11 +16,26 @@
%ul.content-list.commit-list.flex-list
= render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }
+- if context_commits.present?
+ %li.commit-header.js-commit-header
+ %span.font-weight-bold= n_("%d previously merged commit", "%d previously merged commits", context_commits.count) % context_commits.count
+ - if project.context_commits_enabled? && can_update_merge_request
+ %button.btn.btn-default.ml-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'false' } }
+ = _('Add/remove')
+
+ %li.commits-row
+ %ul.content-list.commit-list.flex-list
+ = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
+
- if hidden > 0
%li.alert.alert-warning
= n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
-- if commits.size == 0
+- if project.context_commits_enabled? && can_update_merge_request && context_commits&.empty?
+ %button.btn.btn-default.mt-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'true' } }
+ = _('Add previously merged commits')
+
+- if commits.size == 0 && context_commits.nil?
.mt-4.text-center
.bold
= _('Your search didn\'t match any commits.')
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 737e4f66dd2..28b5dc0cc67 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -26,8 +26,8 @@
= form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do
= search_field_tag :search, params[:search], { placeholder: _('Search by message'), id: 'commits-search', class: 'form-control search-text-input input-short gl-mt-3 gl-sm-mt-0 gl-min-w-full', spellcheck: false }
.control.d-none.d-md-block
- = link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
- = icon("rss")
+ = link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn btn-svg' do
+ = sprite_icon('rss', css_class: 'qa-rss-icon')
= render_if_exists 'projects/commits/mirror_status'
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index d10fa69ff47..768acac96c0 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -10,7 +10,7 @@
= hidden_field_tag :to, params[:to]
= button_tag type: 'button', title: params[:to], class: "btn form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text.str-truncated.monospace.float-left= params[:to] || _("Select branch/tag")
- = sprite_icon('chevron-down', size: 16, css_class: 'float-right')
+ = sprite_icon('chevron-down', css_class: 'float-right')
= render 'shared/ref_dropdown'
.compare-ellipsis.inline ...
.form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
@@ -21,7 +21,7 @@
= hidden_field_tag :from, params[:from]
= button_tag type: 'button', title: params[:from], class: "btn form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
.dropdown-toggle-text.str-truncated.monospace.float-left= params[:from] || _("Select branch/tag")
- = sprite_icon('chevron-down', size: 16, css_class: 'float-right')
+ = sprite_icon('chevron-down', css_class: 'float-right')
= render 'shared/ref_dropdown'
&nbsp;
= button_tag s_("CompareBranches|Compare"), class: "btn btn-success commits-compare-btn"
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 93ee1bed809..e45ea209e8c 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -8,9 +8,9 @@
%code.ref-name master
- example_sha = capture do
%code.ref-name 4eedf23
- = (_("Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request.") % { master: example_master, sha: example_sha }).html_safe
+ = html_escape(_("Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request.")) % { master: example_master.html_safe, sha: example_sha.html_safe }
%br
- = (_("Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision.")).html_safe
+ = html_escape(_("Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision.")) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe }
.prepend-top-20
= render "form"
diff --git a/app/views/projects/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml
index 38bec0361b0..b78535bbe2f 100644
--- a/app/views/projects/default_branch/_show.html.haml
+++ b/app/views/projects/default_branch/_show.html.haml
@@ -9,7 +9,7 @@
= _('Select the branch you want to set as the default for this project. All merge requests and commits will automatically be made against this branch unless you specify a different one.')
.settings-content
- = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f|
+ = form_for @project, remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f|
%fieldset
- if @project.empty_repo?
.text-secondary
@@ -28,4 +28,5 @@
= _("Issues referenced by merge requests and commits within the default branch will be closed automatically")
= link_to icon('question-circle'), help_page_path('user/project/issues/managing_issues.md', anchor: 'disabling-automatic-issue-closing'), target: '_blank'
- = f.submit 'Save changes', class: "btn btn-success"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml
index 7fa7036245c..805d4983002 100644
--- a/app/views/projects/deploy_keys/edit.html.haml
+++ b/app/views/projects/deploy_keys/edit.html.haml
@@ -3,7 +3,7 @@
%hr
%div
- = form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], include_id: false, html: { class: 'js-requires-input' } do |f|
+ = 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: 'btn-success btn'
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index c84c376d57b..5127d8b77d5 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -10,5 +10,5 @@
- actions.each do |action|
- next unless can?(current_user, :update_build, action)
%li
- = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
+ = link_to [:play, @project, action], method: :post, rel: 'nofollow' do
%span= action.name
diff --git a/app/views/projects/deployments/_confirm_rollback_modal.html.haml b/app/views/projects/deployments/_confirm_rollback_modal.html.haml
index 9162827b501..3735ead1559 100644
--- a/app/views/projects/deployments/_confirm_rollback_modal.html.haml
+++ b/app/views/projects/deployments/_confirm_rollback_modal.html.haml
@@ -16,7 +16,7 @@
= s_('Environments|This action will run the job defined by %{environment_name} for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?').html_safe % {commit_id: commit_sha, environment_name: @environment.name}
.modal-footer
= button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
- = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-danger' do
+ = link_to [:retry, @project, deployment.deployable], method: :post, class: 'btn btn-danger' do
- if deployment.last?
= s_('Environments|Re-deploy')
- else
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 4b76dde681e..6ba363e6555 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -3,6 +3,7 @@
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
- diff_page_context = local_assigns.fetch(:diff_page_context, nil)
+- load_diff_files_async = Feature.enabled?(:async_commit_diff_files, @project) && diff_page_context == "is-commit"
.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed
.files-changed-inner
@@ -26,5 +27,12 @@
- if render_overflow_warning?(diffs)
= render 'projects/diffs/warning', diff_files: diffs
+
.files{ data: { can_create_note: can_create_note } }
- = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context }
+ - if load_diff_files_async
+ - url = url_for(safe_params.merge(action: 'diff_files'))
+ .js-diffs-batch{ data: { diff_files_path: url } }
+ .text-center
+ %span.spinner.spinner-md
+ - else
+ = render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context }
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 7395c16c38b..bd023e0442c 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -14,7 +14,7 @@
.file-actions.d-none.d-sm-block
- if blob&.readable_text?
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: _("Toggle comments for this file"), disabled: @diff_notes_disabled do
- = sprite_icon('comment', size: 16)
+ = sprite_icon('comment')
\
- if editable_diff?(diff_file)
- link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {}
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 0e2a1165ad3..b438fbbf446 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -22,7 +22,7 @@
- diff_files.each do |diff_file|
%li
%a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path }
- = sprite_icon(diff_file_changed_icon(diff_file), size: 16, css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon gl-mr-3")
+ = sprite_icon(diff_file_changed_icon(diff_file), css_class: "#{diff_file_changed_icon_color(diff_file)} diff-file-changed-icon gl-mr-3")
%span.diff-changed-file-content.gl-mr-3
- if diff_file.file_path
%strong.diff-changed-file-name
diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml
index 2cc3d921abc..643d111fedd 100644
--- a/app/views/projects/diffs/_warning.html.haml
+++ b/app/views/projects/diffs/_warning.html.haml
@@ -9,4 +9,4 @@
= link_to _("Plain diff"), merge_request_path(@merge_request, format: :diff), class: "btn btn-sm"
= link_to _("Email patch"), merge_request_path(@merge_request, format: :patch), class: "btn btn-sm"
%p
- = _("To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed.").html_safe % { display_size: diff_files.size, real_size: diff_files.real_size }
+ = html_escape(_("To preserve performance only %{strong_open}%{display_size} of %{real_size}%{strong_close} files are displayed.")) % { display_size: diff_files.size, real_size: diff_files.real_size, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index e63b615115a..bf978b01652 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -17,13 +17,14 @@
%p= _('Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions.')
.settings-content
- = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
+ = form_for @project, remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' }
%template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project)
.js-project-permissions-form
- - if show_visibility_confirm_modal?(@project)
- = render "visibility_modal"
- = f.submit _('Save changes'), class: "btn btn-success #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
+ .gl-display-flex.gl-justify-content-end
+ - if show_visibility_confirm_modal?(@project)
+ = render "visibility_modal"
+ = f.submit _('Save changes'), class: "btn btn-success #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
%section.qa-merge-request-settings.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] }
.settings-header
@@ -34,10 +35,11 @@
.settings-content
= render_if_exists 'shared/promotions/promote_mr_features'
- = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
+ = form_for @project, remote: true, 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 btn-success qa-save-merge-request-changes rspec-save-merge-request-changes"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: "btn btn-succes qa-save-merge-request-changes rspec-save-merge-request-changes"
= render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded
@@ -68,8 +70,9 @@
.sub-section
%h4= _('Housekeeping')
%p= _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.')
- = link_to _('Run housekeeping'), housekeeping_project_path(@project),
- method: :post, class: "btn btn-default"
+ .gl-display-flex.gl-justify-content-end
+ = link_to _('Run housekeeping'), housekeeping_project_path(@project),
+ method: :post, class: "btn btn-default"
= render 'export', project: @project
@@ -77,7 +80,7 @@
.sub-section.rename-repository
%h4.warning-title= _('Change path')
= render 'projects/errors'
- = form_for([@project.namespace.becomes(Namespace), @project]) do |f|
+ = form_for @project do |f|
.form-group
= f.label :path, _('Path'), class: 'label-bold'
.form-group
@@ -91,12 +94,13 @@
%li= _('You will need to update your local repositories to point to the new location.')
- if @project.deployment_platform.present?
%li= _('Your deployment services will be broken, you will need to manually fix the services after renaming.')
- = f.submit _('Change path'), class: "btn btn-warning qa-change-path-button"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Change path'), class: "btn btn-warning qa-change-path-button"
- if can?(current_user, :change_namespace, @project)
.sub-section
%h4.danger-title= _('Transfer project')
- = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f|
+ = form_for @project, url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } do |f|
.form-group
= label_tag :new_namespace_id, nil, class: 'label-bold' do
%span= _('Select a new namespace')
@@ -107,14 +111,15 @@
%li= _('You can only transfer the project to namespaces you manage.')
%li= _('You will need to update your local repositories to point to the new location.')
%li= _('Project visibility level will be changed to match namespace rules when transferring to a group.')
- = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger qa-transfer-button", data: { "confirm-danger-message" => transfer_project_message(@project) }
+ .gl-display-flex.gl-justify-content-end
+ = f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger qa-transfer-button", data: { "confirm-danger-message" => transfer_project_message(@project) }
- if @project.forked? && can?(current_user, :remove_fork_project, @project)
.sub-section
%h4.danger-title= _('Remove fork relationship')
%p= remove_fork_project_description_message(@project)
- = form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|
+ = form_for @project, url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' } do |f|
%p
%strong= _('Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.')
= button_to _('Remove fork relationship'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) }
diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml
index 39eda493d69..0b8f9fe220d 100644
--- a/app/views/projects/environments/_form.html.haml
+++ b/app/views/projects/environments/_form.html.haml
@@ -6,7 +6,7 @@
- link_to_read_more = link_to(_("More information"), help_page_path("ci/environments/index.md"))
= _("Environments allow you to track deployments of your application %{link_to_read_more}.").html_safe % { link_to_read_more: link_to_read_more }
- = form_for [@project.namespace.becomes(Namespace), @project, @environment], html: { class: 'col-lg-9' } do |f|
+ = form_for [@project, @environment], html: { class: 'col-lg-9' } do |f|
= form_errors(@environment)
.form-group
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index d5249662dde..2e665a12a0a 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -65,7 +65,7 @@
%h4.state-title
= _("You don't have any deployments right now.")
%p.blank-state-text
- = _("Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here.").html_safe
+ = html_escape(_("Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here.")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
.text-center
= link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success"
- else
diff --git a/app/views/projects/forks/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml
index eec02a50b85..dd49e8bdb4b 100644
--- a/app/views/projects/forks/_fork_button.html.haml
+++ b/app/views/projects/forks/_fork_button.html.haml
@@ -1,26 +1,20 @@
- avatar = namespace_icon(namespace, 100)
- can_create_project = current_user.can?(:create_projects, namespace)
-- if forked_project = namespace.find_fork_of(@project)
- .bordered-box.fork-thumbnail.text-center.gl-ml-3.gl-mr-3.gl-mt-3.gl-mb-3.forked
- = link_to project_path(forked_project) do
- - if /no_((\w*)_)*avatar/.match(avatar)
- = group_icon(namespace, class: "avatar rect-avatar s100 identicon mx-auto")
- - else
- .avatar-container.s100.mx-auto
- = image_tag(avatar, class: "avatar s100")
- %h5.gl-mt-3
- = namespace.human_name
-- else
- .bordered-box.fork-thumbnail.text-center.gl-ml-3.gl-mr-3.gl-mt-3.gl-mb-3{ class: ("disabled" unless can_create_project) }
- = link_to project_forks_path(@project, namespace_key: namespace.id),
- method: "POST",
- class: ("disabled has-tooltip" unless can_create_project),
- title: (_('You have reached your project limit') unless can_create_project) do
- - if /no_((\w*)_)*avatar/.match(avatar)
- = group_icon(namespace, class: "avatar rect-avatar s100 identicon mx-auto")
- - else
- .avatar-container.s100.mx-auto
- = image_tag(avatar, class: "avatar s100")
- %h5.gl-mt-3{ data: { qa_selector: 'fork_namespace_content', qa_name: namespace.human_name } }
- = namespace.human_name
+.bordered-box.fork-thumbnail.text-center.gl-m-3{ class: ("disabled" unless can_create_project) }
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ = group_icon(namespace, class: "avatar rect-avatar s100 identicon mx-auto")
+ - else
+ .avatar-container.s100.mx-auto.gl-mt-5
+ = image_tag(avatar, class: "avatar s100")
+ %h5.gl-mt-3
+ = namespace.human_name
+ - if forked_project = namespace.find_fork_of(@project)
+ = link_to _("Go to project"), project_path(forked_project), class: "btn"
+ - else
+ %div{ class: ('has-tooltip' unless can_create_project),
+ title: (_('You have reached your project limit') unless can_create_project) }
+ = link_to _("Select"), project_forks_path(@project, namespace_key: namespace.id),
+ data: { qa_selector: 'fork_namespace_button', qa_name: namespace.human_name },
+ method: "POST",
+ class: ["btn btn-success", ("disabled" unless can_create_project)]
diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml
index b37dba8b35d..5d527f1bcfb 100644
--- a/app/views/projects/forks/error.html.haml
+++ b/app/views/projects/forks/error.html.haml
@@ -2,7 +2,7 @@
- if @forked_project && !@forked_project.saved?
.alert.alert-danger.alert-block
%h4
- = sprite_icon('fork', size: 16)
+ = sprite_icon('fork')
= _("Fork Error!")
%p
= _("You tried to fork %{link_to_the_project} but it failed for the following reason:").html_safe % { link_to_the_project: link_to_project(@project) }
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index 887081d0f35..45d314a1088 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -5,17 +5,14 @@
%h4.gl-mt-0
= _("Fork project")
%p
- = _("A fork is a copy of a project.<br />Forking a repository allows you to make changes without affecting the original project.").html_safe
+ = _("A fork is a copy of a project.")
+ %br
+ = _('Forking a repository allows you to make changes without affecting the original project.')
.col-lg-9
- - if @namespaces.present?
+ - if @own_namespace.present?
.fork-thumbnail-container.js-fork-content
%h5.gl-mt-0.gl-mb-0.gl-ml-3.gl-mr-3
= _("Select a namespace to fork the project")
- - @namespaces.each do |namespace|
- = render 'fork_button', namespace: namespace
- - else
- %strong
- = _("No available namespaces to fork the project.")
- %p.gl-mt-3
- = _("You must have permission to create a project in a namespace before forking.")
+ = render 'fork_button', namespace: @own_namespace
+ #fork-groups-mount-element{ data: { endpoint: new_project_fork_path(@project, format: :json), can_create_project: current_user.can_create_project?.to_s } }
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
index e0ef0c0d3f9..f728ef5ac1a 100644
--- a/app/views/projects/hooks/edit.html.haml
+++ b/app/views/projects/hooks/edit.html.haml
@@ -7,7 +7,7 @@
= render 'shared/web_hooks/title_and_docs', hook: @hook
.col-lg-9.gl-mb-3
- = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: project_hook_path(@project, @hook) do |f|
+ = 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 }
%span>= f.submit 'Save changes', class: 'btn btn-success gl-mr-3'
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index 1845bd190d3..5c6a87ddb26 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -7,8 +7,9 @@
= render 'shared/web_hooks/title_and_docs', hook: @hook
.col-lg-8.gl-mb-3
- = form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f|
+ = 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: 'btn btn-success'
+ .gl-display-flex.gl-justify-content-end
+ = f.submit 'Add webhook', class: 'btn btn-success'
= render 'shared/web_hooks/index', hooks: @hooks, hook_class: @hook.class
diff --git a/app/views/projects/incidents/index.html.haml b/app/views/projects/incidents/index.html.haml
new file mode 100644
index 00000000000..3d66c254601
--- /dev/null
+++ b/app/views/projects/incidents/index.html.haml
@@ -0,0 +1,3 @@
+- page_title _('Incidents')
+
+#js-incidents{ data: incidents_data(@project) }
diff --git a/app/views/projects/issues/_alert_moved_from_service_desk.html.haml b/app/views/projects/issues/_alert_moved_from_service_desk.html.haml
index a6f969f8b10..9b142b08574 100644
--- a/app/views/projects/issues/_alert_moved_from_service_desk.html.haml
+++ b/app/views/projects/issues/_alert_moved_from_service_desk.html.haml
@@ -3,8 +3,8 @@
- service_desk_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: service_desk_link_url }
.hide.gl-alert.gl-alert-warning.js-alert-moved-from-service-desk-warning.gl-mt-5{ role: 'alert' }
- = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ = sprite_icon('warning', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
%button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ = sprite_icon('close', css_class: 'gl-icon')
.gl-alert-body.gl-mr-3
= s_('This project does not have %{service_desk_link_start}Service Desk%{service_desk_link_end} enabled, so the user who created the issue will no longer receive email notifications about new activity.').html_safe % { service_desk_link_start: service_desk_link_start, service_desk_link_end: '</a>'.html_safe }
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index bcc74e8d1d9..4273130bbc2 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,3 +1,5 @@
+- add_page_startup_api_call discussions_path(@issue)
+
- @gfm_form = true
- content_for :note_actions do
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 1be1087b36f..dcc8000c0c5 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,3 +1,3 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @issue],
+= form_for [@project, @issue],
html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index e7cd35497e8..ba9ab50cb3a 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,12 +1,12 @@
-# DANGER: Any changes to this file need to be reflected in issuables_list/components/issuable.vue!
-%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', qa_issue_title: issue.title } }
+%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 } }
.issue-box
- if @can_bulk_update
.issue-check.hidden
= check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected-issuable"
.issuable-info-container
.issuable-main-info
- .issue-title.title.d-flex.align-items-center
+ .issue-title.title
%span.issue-title-text.js-onboarding-issue-item{ dir: "auto" }
- if issue.confidential?
%span.has-tooltip{ title: _('Confidential') }
@@ -30,7 +30,7 @@
%span.issuable-milestone.d-none.d-sm-inline-block
&nbsp;
= link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 'true', toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do
- = icon('clock-o')
+ = sprite_icon('clock', css_class: 'gl-vertical-align-text-bottom')
= issue.milestone.title
- if issue.due_date
%span.issuable-due-date.d-none.d-sm-inline-block.has-tooltip{ class: "#{'cred' if issue.overdue?}", title: _('Due date') }
diff --git a/app/views/projects/issues/_issue_estimate.html.haml b/app/views/projects/issues/_issue_estimate.html.haml
index 46797d0f1a0..c49bf626f4e 100644
--- a/app/views/projects/issues/_issue_estimate.html.haml
+++ b/app/views/projects/issues/_issue_estimate.html.haml
@@ -3,5 +3,5 @@
- if issue.time_estimate > 0
%span.issuable-estimate.d-none.d-sm-inline-block.has-tooltip{ data: { container: 'body', qa_selector: 'issuable_estimate' }, title: _('Estimate') }
&nbsp;
- = sprite_icon('timer', size: 16, css_class: 'issue-estimate-icon')
+ = sprite_icon('timer', css_class: 'issue-estimate-icon')
= Gitlab::TimeTrackingFormatter.output(issue.time_estimate)
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index c0383c57e63..1e24b08ece2 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,8 +1,13 @@
- if Feature.enabled?(:vue_issuables_list, @project)
- .js-issuables-list{ data: { endpoint: expose_url(api_v4_projects_issues_path(id: @project.id)),
+ - data_endpoint = local_assigns.fetch(:data_endpoint, expose_path(api_v4_projects_issues_path(id: @project.id)))
+ - default_empty_state_meta = { create_issue_path: new_project_issue_path(@project), svg_path: image_path('illustrations/issues.svg') }
+ - data_empty_state_meta = local_assigns.fetch(:data_empty_state_meta, default_empty_state_meta)
+ - type = local_assigns.fetch(:type, '')
+ .js-issuables-list{ data: { endpoint: data_endpoint,
+ 'empty-state-meta': data_empty_state_meta.to_json,
'can-bulk-edit': @can_bulk_update.to_json,
- 'empty-svg-path': image_path('illustrations/issues.svg'),
- 'sort-key': @sort } }
+ 'sort-key': @sort,
+ 'type': type } }
- else
- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position') }
diff --git a/app/views/projects/issues/_service_desk_empty_state.html.haml b/app/views/projects/issues/_service_desk_empty_state.html.haml
new file mode 100644
index 00000000000..4f004439f45
--- /dev/null
+++ b/app/views/projects/issues/_service_desk_empty_state.html.haml
@@ -0,0 +1,33 @@
+- service_desk_enabled = @project.service_desk_enabled?
+
+- can_edit_project_settings = can?(current_user, :admin_project, @project)
+- title_text = _("Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab")
+
+- if Gitlab::ServiceDesk.supported?
+ .empty-state
+ .svg-content
+ = render 'shared/empty_states/icons/service_desk_empty_state.svg'
+
+ .text-content
+ %h4= title_text
+
+ - if can_edit_project_settings && service_desk_enabled
+ %p
+ = _("Have your users email")
+ %code= @project.service_desk_address
+
+ %span= _("Those emails automatically become issues (with the comments becoming the email conversation) listed here.")
+ = link_to _('Read more'), help_page_path('user/project/service_desk')
+
+ - if can_edit_project_settings && !service_desk_enabled
+ .text-center
+ = link_to _("Turn on Service Desk"), edit_project_path(@project), class: 'btn btn-success'
+- else
+ .empty-state
+ .svg-content
+ = render 'shared/empty_states/icons/service_desk_setup.svg'
+ .text-content
+ %h4= _('Service Desk is enabled but not yet active')
+ %p
+ = _("You must set up incoming email before it becomes active.")
+ = link_to _('More information'), help_page_path('administration/incoming_email', anchor: 'set-it-up')
diff --git a/app/views/projects/issues/_service_desk_info_content.html.haml b/app/views/projects/issues/_service_desk_info_content.html.haml
index ddd8e545043..7fa2f3fab00 100644
--- a/app/views/projects/issues/_service_desk_info_content.html.haml
+++ b/app/views/projects/issues/_service_desk_info_content.html.haml
@@ -1,39 +1,23 @@
-- is_empty_state = @issues.blank?
- service_desk_enabled = @project.service_desk_enabled?
-- callout_selector = is_empty_state ? 'empty-state' : 'non-empty-state media'
-- svg_path = !is_empty_state ? 'shared/empty_states/icons/service_desk_callout.svg' : 'shared/empty_states/icons/service_desk_empty_state.svg'
- can_edit_project_settings = can?(current_user, :admin_project, @project)
- title_text = _("Use Service Desk to connect with your users (e.g. to offer customer support) through email right inside GitLab")
-- if Gitlab::ServiceDesk.supported?
- %div{ class: "#{callout_selector}" }
- .svg-content
- = render svg_path
+.non-empty-state.media
+ .svg-content
+ = render 'shared/empty_states/icons/service_desk_callout.svg'
- %div{ class: is_empty_state ? "text-content" : "prepend-top-10 gl-ml-3" }
- - if is_empty_state
- %h4= title_text
- - else
- %h5= title_text
+ .gl-mt-3.gl-ml-3
+ %h5= title_text
- - if can_edit_project_settings && service_desk_enabled
- %p
- = _("Have your users email")
- %code= @project.service_desk_address
+ - if can_edit_project_settings && service_desk_enabled
+ %p
+ = _("Have your users email")
+ %code= @project.service_desk_address
- %span= _("Those emails automatically become issues (with the comments becoming the email conversation) listed here.")
- = link_to _('Read more'), help_page_path('user/project/service_desk')
+ %span= _("Those emails automatically become issues (with the comments becoming the email conversation) listed here.")
+ = link_to _('Read more'), help_page_path('user/project/service_desk')
- - if can_edit_project_settings && !service_desk_enabled
- %div{ class: is_empty_state ? "text-center" : "prepend-top-10" }
- = link_to _("Turn on Service Desk"), edit_project_path(@project), class: 'btn btn-success'
-- else
- .empty-state
- .svg-content
- = render 'shared/empty_states/icons/service_desk_setup.svg'
- .text-content
- %h4= _('Service Desk is enabled but not yet active')
- %p
- = _("You must set up incoming email before it becomes active.")
- = link_to _('More information'), help_page_path('administration/incoming_email', anchor: 'set-it-up')
+ - if can_edit_project_settings && !service_desk_enabled
+ .gl-mt-3
+ = link_to _("Turn on Service Desk"), edit_project_path(@project), class: 'btn btn-success'
diff --git a/app/views/projects/issues/export_csv/_modal.html.haml b/app/views/projects/issues/export_csv/_modal.html.haml
index 342c3ba27bb..793e43da935 100644
--- a/app/views/projects/issues/export_csv/_modal.html.haml
+++ b/app/views/projects/issues/export_csv/_modal.html.haml
@@ -8,7 +8,7 @@
.svg-content.import-export-svg-container
= image_tag 'illustrations/export-import.svg', alt: _('Import/Export illustration'), class: 'illustration'
%a.close{ href: '#', 'data-dismiss' => 'modal' }
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ = sprite_icon('close', css_class: 'gl-icon')
.modal-body
.modal-subheader
= icon('check', { class: 'checkmark' })
@@ -16,6 +16,6 @@
- issues_count = issuables_count_for_state(:issues, params[:state])
= n_('%d issue selected', '%d issues selected', issues_count) % issues_count
.modal-text
- = _('The CSV export will be created in the background. Once finished, it will be sent to <strong>%{email}</strong> in an attachment.').html_safe % { email: @current_user.notification_email }
+ = html_escape(_('The CSV export will be created in the background. Once finished, it will be sent to %{strong_open}%{email}%{strong_close} in an attachment.')) % { email: @current_user.notification_email, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
.modal-footer
= link_to _('Export issues'), export_csv_project_issues_path(@project, request.query_parameters), method: :post, class: 'btn btn-success float-left', title: _('Export issues'), data: { track_label: "export_issues_csv", track_event: "click_button", track_value: "", qa_selector: "export_issues_button" }
diff --git a/app/views/projects/issues/service_desk.html.haml b/app/views/projects/issues/service_desk.html.haml
index 9b0b3ebc9e0..bd260bdf143 100644
--- a/app/views/projects/issues/service_desk.html.haml
+++ b/app/views/projects/issues/service_desk.html.haml
@@ -5,9 +5,11 @@
- content_for :breadcrumbs_extra do
= render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false
-- support_bot_attrs = UserSerializer.new.represent(User.support_bot).to_json
+- support_bot_attrs = { service_desk_enabled: @project.service_desk_enabled?, **UserSerializer.new.represent(User.support_bot) }.to_json
-%div{ class: "js-service-desk-issues service-desk-issues", data: { support_bot: support_bot_attrs } }
+- data_endpoint = "#{expose_path(api_v4_projects_issues_path(id: @project.id))}?author_id=#{User.support_bot.id}"
+
+%div{ class: "js-service-desk-issues service-desk-issues", data: { support_bot: support_bot_attrs, service_desk_meta: service_desk_meta(@project) } }
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls.d-block.d-sm-none
@@ -15,7 +17,15 @@
- if @issues.present?
= render 'shared/issuable/search_bar', type: :issues
- = render 'service_desk_info_content'
+ - if Gitlab::ServiceDesk.supported?
+ = render 'service_desk_info_content'
+ -# TODO Remove empty_state_path once vue_issuables_list FF is removed.
+ -# https://gitlab.com/gitlab-org/gitlab/-/issues/235652
+ -# `empty_state_path` is used to render the empty state in the HAML version of issuables list.
.issues-holder
- = render 'projects/issues/issues', empty_state_path: 'service_desk_info_content'
+ = render 'projects/issues/issues',
+ empty_state_path: 'service_desk_empty_state',
+ data_endpoint: data_endpoint,
+ data_empty_state_meta: service_desk_meta(@project),
+ type: 'service_desk'
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 2a0dc5e30b9..a7817ad5552 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -9,6 +9,7 @@
- can_reopen_issue = can?(current_user, :reopen_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project)
+- related_branches_path = related_branches_project_issue_path(@project, @issue)
= render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user
= render "projects/issues/alert_moved_from_service_desk", issue: @issue
@@ -16,11 +17,11 @@
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(@issue, status_box: :closed) }
- = sprite_icon('mobile-issue-close', size: 16, css_class: 'd-block d-sm-none')
+ = sprite_icon('mobile-issue-close', css_class: 'd-block d-sm-none')
.d-none.d-sm-block
= issue_closed_text(@issue, current_user)
.issuable-status-box.status-box.status-box-open{ class: issue_status_visibility(@issue, status_box: :open) }
- = sprite_icon('issue-open-m', size: 16, css_class: 'd-block d-sm-none')
+ = sprite_icon('issue-open-m', css_class: 'd-block d-sm-none')
%span.d-none.d-sm-block Open
.issuable-meta
@@ -82,7 +83,8 @@
#js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project)
- #related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
+ - add_page_startup_api_call related_branches_path
+ #related-branches{ data: { url: related_branches_path } }
-# This element is filled in using JavaScript.
.content-block.emoji-block.emoji-block-sticky
diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml
index 6da7c317f3a..935a3493a37 100644
--- a/app/views/projects/issues/verify.html.haml
+++ b/app/views/projects/issues/verify.html.haml
@@ -1,5 +1,3 @@
-- form = [@project.namespace.becomes(Namespace), @project, @issue]
-
-= render layout: 'layouts/recaptcha_verification', locals: { spammable: @issue, form: form } do
+= render layout: 'layouts/recaptcha_verification', locals: { spammable: @issue } do
= hidden_field_tag(:merge_request_to_resolve_discussions_of, params[:merge_request_to_resolve_discussions_of])
= hidden_field_tag(:discussion_to_resolve, params[:discussion_to_resolve])
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index ba47712211d..8d8270847a3 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -8,7 +8,7 @@
#promote-label-modal
= render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label
- .labels-container.prepend-top-10
+ .labels-container.gl-mt-3
- if can_admin_label && search.blank?
%p.text-muted
= _('Labels can be applied to issues and merge requests.')
@@ -18,7 +18,7 @@
-# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: [('hide' if hide), ('is-not-draggable' unless can_admin_label)] }
- %h5.prepend-top-10= _('Prioritized Labels')
+ %h5.gl-mt-3= _('Prioritized Labels')
.content-list.manage-labels-list.js-prioritized-labels{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } }
#js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" }
= render 'shared/empty_states/priority_labels'
diff --git a/app/views/projects/merge_requests/_approvals_count.html.haml b/app/views/projects/merge_requests/_approvals_count.html.haml
index 464cba1bb2d..449e75f26e0 100644
--- a/app/views/projects/merge_requests/_approvals_count.html.haml
+++ b/app/views/projects/merge_requests/_approvals_count.html.haml
@@ -6,7 +6,7 @@
- final_text = n_("%d approver", "%d approvers", total) % total
- final_self_text = n_("%d approver (you've approved)", "%d approvers (you've approved)", total) % total
- - approval_icon = sprite_icon((self_approved ? 'approval-solid' : 'approval'), size: 16, css_class: 'align-middle')
+ - approval_icon = sprite_icon((self_approved ? 'approval-solid' : 'approval'), css_class: 'align-middle')
%li.d-none.d-sm-inline-block.has-tooltip.text-success{ title: self_approved ? final_self_text : final_text }
= approval_icon
diff --git a/app/views/projects/merge_requests/_awards_block.html.haml b/app/views/projects/merge_requests/_awards_block.html.haml
index e4a7b9b7e62..e7577e13b68 100644
--- a/app/views/projects/merge_requests/_awards_block.html.haml
+++ b/app/views/projects/merge_requests/_awards_block.html.haml
@@ -1,6 +1,5 @@
.content-block.content-block-small.emoji-list-container.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true do
- - if mr_tabs_position_enabled?
- .ml-auto.mt-auto.mb-auto
- #js-vue-sort-issue-discussions
- = render "projects/merge_requests/discussion_filter"
+ .ml-auto.mt-auto.mb-auto
+ #js-vue-sort-issue-discussions
+ = render "projects/merge_requests/discussion_filter"
diff --git a/app/views/projects/merge_requests/_commits.html.haml b/app/views/projects/merge_requests/_commits.html.haml
index b414518b597..178e57b08b3 100644
--- a/app/views/projects/merge_requests/_commits.html.haml
+++ b/app/views/projects/merge_requests/_commits.html.haml
@@ -1,8 +1,18 @@
-- if @commits.empty?
- .commits-empty
- %h4
- There are no commits yet.
+- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
+
+- if @commits.empty? && @context_commits.empty?
+ .commits-empty.mt-5
= custom_icon ('illustration_no_commits')
+ %h4
+ = _('There are no commits yet.')
+ - if @project&.context_commits_enabled? && can_update_merge_request
+ %p
+ = _('Push commits to the source branch or add previously merged commits to review them.')
+ %button.btn.btn-primary.add-review-item-modal-trigger{ type: "button", data: { commits_empty: 'true', context_commits_empty: 'true' } }
+ = _('Add previously merged commits')
- else
%ol#commits-list.list-unstyled
= render "projects/commits/commits", merge_request: @merge_request
+
+- if @project&.context_commits_enabled? && can_update_merge_request && @merge_request.iid
+ .add-review-item-modal-wrapper{ data: { context_commits_path: context_commits_project_json_merge_request_url(@merge_request&.project, @merge_request, :json), target_branch: @merge_request.target_branch, merge_request_iid: @merge_request.iid, project_id: @merge_request.project.id } }
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index a7c9e54506d..a68a4318538 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,3 +1,3 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request],
+= form_for [@project, @merge_request],
html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request, presenter: @mr_presenter
diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml
index a2da0e707d3..df81e608c3e 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -32,12 +32,12 @@
- if @merge_request.for_fork?
:preserve
git fetch origin
- git checkout "origin/#{h @merge_request.target_branch}"
+ git checkout "#{h @merge_request.target_branch}"
git merge --no-ff "#{h @merge_request.source_project_path}-#{h @merge_request.source_branch}"
- else
:preserve
git fetch origin
- git checkout "origin/#{h @merge_request.target_branch}"
+ git checkout "#{h @merge_request.target_branch}"
git merge --no-ff "#{h @merge_request.source_branch}"
%p
%strong Step 4.
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index d3e98bac7f9..ad0f4d03f9a 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -25,7 +25,7 @@
%span.issuable-milestone.d-none.d-sm-inline-block
&nbsp;
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 'true', toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
- = icon('clock-o')
+ = sprite_icon('clock', css_class: 'gl-vertical-align-text-bottom')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
%span.project-ref-path.has-tooltip{ title: _('Target branch') }
@@ -45,13 +45,13 @@
= _('MERGED')
- elsif merge_request.closed?
%li.issuable-status.d-none.d-sm-inline-block
- = icon('ban')
+ = sprite_icon('cancel', css_class: 'gl-vertical-align-text-bottom')
= _('CLOSED')
= render 'shared/merge_request_pipeline_status', merge_request: merge_request
- if merge_request.open? && merge_request.broken?
%li.issuable-pipeline-broken.d-none.d-sm-flex
= link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do
- = icon('exclamation-triangle')
+ = sprite_icon('warning-solid')
- if merge_request.assignees.any?
%li.d-flex
= render 'shared/issuable/assignees', project: merge_request.project, issuable: merge_request
diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml
index ec78b040167..c38cf62b36c 100644
--- a/app/views/projects/merge_requests/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/_mr_box.html.haml
@@ -1,6 +1,3 @@
-.detail-page-description{ class: ("py-2" if mr_tabs_position_enabled?) }
- %h2.title.qa-title{ class: ("mb-0" if mr_tabs_position_enabled?) }
+.detail-page-description.py-2
+ %h2.title.qa-title.mb-0
= markdown_field(@merge_request, :title)
-
- - unless mr_tabs_position_enabled?
- = render "projects/merge_requests/description"
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 72931448432..8aa4a935384 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -7,16 +7,15 @@
.alert.alert-danger
The source project of this merge request has been removed.
-.detail-page-header{ class: ("border-bottom-0 pt-0 pb-0" if mr_tabs_position_enabled?) }
+.detail-page-header.border-bottom-0.pt-0.pb-0
.detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
- = sprite_icon(state_icon_name, size: 16, css_class: 'd-block d-sm-none')
+ = sprite_icon(state_icon_name, css_class: 'd-block d-sm-none')
%span.d-none.d-sm-block
= state_human_name
.issuable-meta
- - if @merge_request.discussion_locked?
- .issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
+ #js-issuable-header-warnings
= issuable_meta(@merge_request, @project, "Merge request")
%a.btn.btn-default.float-right.d-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index 16b08cbf648..64b14f8889c 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -1,6 +1,3 @@
-- if @merge_request.source_branch_exists?
- = render "projects/merge_requests/how_to_merge"
-
= javascript_tag nonce: true do
:plain
window.gl = window.gl || {};
diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml
index 99537ba8152..874adb19734 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -1,7 +1,7 @@
%h3.page-title
New Merge Request
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form js-requires-input" } do |f|
+= form_for [@project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form js-requires-input" } do |f|
- if params[:nav_source].present?
= hidden_field_tag(:nav_source, params[:nav_source])
.hide.alert.alert-danger.mr-compare-errors
diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml
index fdf0bfe8e50..79781e4a311 100644
--- a/app/views/projects/merge_requests/creations/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml
@@ -1,6 +1,6 @@
%h3.page-title
New Merge Request
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
+= form_for [@project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits, presenter: @mr_presenter
= f.hidden_field :source_project_id
= f.hidden_field :source_branch
diff --git a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
index efc052ca791..c022d2c70d8 100644
--- a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
+++ b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
@@ -2,8 +2,10 @@
WARNING: Please keep changes up-to-date with the following files:
- `assets/javascripts/diffs/components/commit_widget.vue`
-#-----------------------------------------------------------------
+- collapsible = local_assigns.fetch(:collapsible, true)
+
- if @commit
- .info-well.d-none.d-sm-block.gl-mt-3
+ .info-well.mw-100.mx-0
.well-segment
%ul.blob-commit-info
- = render 'projects/commits/commit', commit: @commit, merge_request: @merge_request, view_details: true
+ = render 'projects/commits/commit', commit: @commit, merge_request: @merge_request, view_details: true, collapsible: collapsible
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 03fa9758587..746d613934c 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -12,22 +12,17 @@
.merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version } }
= render "projects/merge_requests/mr_title"
+ - if @merge_request.source_branch_exists?
+ = render "projects/merge_requests/how_to_merge"
+
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
= render "projects/merge_requests/mr_box"
-
- - unless mr_tabs_position_enabled?
- = render "projects/merge_requests/widget"
- = render "projects/merge_requests/awards_block"
-
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
.merge-request-tabs-container
%ul.merge-request-tabs.nav-tabs.nav.nav-links
= render "projects/merge_requests/tabs/tab", class: "notes-tab", qa_selector: "notes_tab" do
= tab_link_for @merge_request, :show, force_link: @commit.present? do
- - if mr_tabs_position_enabled?
- = _("Overview")
- - else
- = _("Discussion")
+ = _("Overview")
%span.badge.badge-pill= @merge_request.related_notes.user.count
- if @merge_request.source_project
= render "projects/merge_requests/tabs/tab", name: "commits", class: "commits-tab" do
@@ -43,11 +38,7 @@
= tab_link_for @merge_request, :diffs do
= _("Changes")
%span.badge.badge-pill= @merge_request.diff_size
- - if mr_tabs_position_enabled? && show_tabs_feature_highlight?
- .js-tabs-feature-highlight{ data: { dismiss_endpoint: user_callouts_path, feature_id: UserCalloutsHelper::TABS_POSITION_HIGHLIGHT } }
.d-flex.flex-wrap.align-items-center.justify-content-lg-end
- - unless mr_tabs_position_enabled?
- = render "projects/merge_requests/discussion_filter"
#js-vue-discussion-counter
.tab-content#diff-notes-app
@@ -59,18 +50,18 @@
%section.col-md-12
%script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
.issuable-discussion.js-vue-notes-event
- - if mr_tabs_position_enabled?
- - if @merge_request.description.present?
- .detail-page-description
- = render "projects/merge_requests/description"
- = render "projects/merge_requests/widget"
- = render "projects/merge_requests/awards_block"
+ - if @merge_request.description.present?
+ .detail-page-description
+ = render "projects/merge_requests/description"
+ = render "projects/merge_requests/widget"
+ = render "projects/merge_requests/awards_block"
#js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json,
noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'),
noteable_type: 'MergeRequest',
target_type: 'merge_request',
help_page_path: suggest_changes_help_path,
- current_user_data: @current_user_data} }
+ current_user_data: @current_user_data,
+ is_locked: @merge_request.discussion_locked.to_s } }
= render "projects/merge_requests/tabs/pane", name: "commits", id: "commits", class: "commits" do
-# This tab is always loaded via AJAX
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index eeff91f631c..907af326ec5 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @milestone],
+= form_for [@project, @milestone],
html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f|
= form_errors(@milestone)
.row
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 5239af82ba6..99e626161c4 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -8,7 +8,7 @@
= render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
-- if can?(current_user, :read_issue, @project) && @milestone.total_issues_count.zero?
+- if can?(current_user, :read_issue, @project) && @milestone.total_issues_count == 0
.alert.alert-success.gl-mt-3
%span= _('Assign some issues to this milestone.')
- elsif @milestone.complete? && @milestone.active?
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index 15c9076c1ab..97b04acea31 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -1,10 +1,10 @@
.account-well.gl-mt-3.gl-mb-3
%ul
%li
- = _('The repository must be accessible over <code>http://</code>,
- <code>https://</code>, <code>ssh://</code> or <code>git://</code>.').html_safe
- %li= _('When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.').html_safe
- %li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe
+ = html_escape(_('The repository must be accessible over %{code_open}http://%{code_close},
+ %{code_open}https://%{code_close}, %{code_open}ssh://%{code_close} or %{code_open}git://%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ %li= html_escape(_('When using the %{code_open}http://%{code_close} or %{code_open}https://%{code_close} protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ %li= html_escape(_('Include the username in the URL if required: %{code_open}https://username@gitlab.company.com/group/project.git%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
- minutes = Gitlab.config.gitlab_shell.git_timeout / 60
= _("The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination.") % { number_of_minutes: minutes }
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 38e4fbf73e0..2f55cce70dc 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -3,7 +3,7 @@
- mirror_settings_enabled = can?(current_user, :admin_remote_mirror, @project)
- mirror_settings_class = "#{'expanded' if expanded} #{'js-mirror-settings' if mirror_settings_enabled}".strip
-%section.settings.project-mirror-settings.no-animate#js-push-remote-settings{ class: mirror_settings_class, data: { qa_selector: 'mirroring_repositories_settings_section' } }
+%section.settings.project-mirror-settings.no-animate#js-push-remote-settings{ class: mirror_settings_class, data: { qa_selector: 'mirroring_repositories_settings_content' } }
.settings-header
%h4= _('Mirroring repositories')
%button.btn.js-settings-toggle
@@ -27,16 +27,16 @@
= render 'projects/mirrors/mirror_repos_form', f: f
- .form-check.append-bottom-10
+ .form-check.gl-mb-3
= check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input'
= label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label'
= link_to icon('question-circle'), help_page_path('user/project/protected_branches'), target: '_blank'
- .panel-footer
+ .panel-footer.gl-display-flex.gl-justify-content-end
= f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
- else
.gl-alert.gl-alert-info{ role: 'alert' }
- = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= _('Mirror settings are only available to GitLab administrators.')
@@ -70,9 +70,8 @@
.badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true', qa_selector: 'mirror_error_badge' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
%td
- if mirror_settings_enabled
- .btn-group.mirror-actions-group.pull-right{ role: 'group' }
+ .btn-group.mirror-actions-group.float-right{ role: 'group' }
- if mirror.ssh_key_auth?
= clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key'), qa_selector: 'copy_public_key_button')
= render 'shared/remote_mirror_update_button', remote_mirror: mirror
%button.js-delete-mirror.qa-delete-mirror.rspec-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o')
-
diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml
index 9b5b31bfc15..39ceaedab61 100644
--- a/app/views/projects/mirrors/_mirror_repos_push.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml
@@ -7,7 +7,7 @@
= rm_f.hidden_field :keep_divergent_refs, class: 'js-mirror-keep-divergent-refs-hidden'
= render partial: 'projects/mirrors/ssh_host_keys', locals: { f: rm_f }
= render partial: 'projects/mirrors/authentication_method', locals: { f: rm_f }
- .form-check.append-bottom-10
+ .form-check.gl-mb-3
= check_box_tag :keep_divergent_refs, '1', false, class: 'js-mirror-keep-divergent-refs form-check-input'
= label_tag :keep_divergent_refs, _('Keep divergent refs'), class: 'form-check-label'
= link_to icon('question-circle'), help_page_path('user/project/repository/repository_mirroring', anchor: 'keep-divergent-refs-core'), target: '_blank'
diff --git a/app/views/projects/mirrors/_ssh_host_keys.html.haml b/app/views/projects/mirrors/_ssh_host_keys.html.haml
index 236ede32d31..786918c4970 100644
--- a/app/views/projects/mirrors/_ssh_host_keys.html.haml
+++ b/app/views/projects/mirrors/_ssh_host_keys.html.haml
@@ -6,7 +6,7 @@
%button.btn.btn-inverted.btn-secondary.inline.js-detect-host-keys.gl-mr-3{ type: 'button', data: { qa_selector: 'detect_host_keys' } }
.js-spinner.d-none.spinner.mr-1
= _('Detect host keys')
- .fingerprint-ssh-info.js-fingerprint-ssh-info.prepend-top-10.append-bottom-10{ class: ('collapse' unless mirror.ssh_mirror_url?) }
+ .fingerprint-ssh-info.js-fingerprint-ssh-info.gl-mt-3.gl-mb-3{ class: ('collapse' unless mirror.ssh_mirror_url?) }
%label.label-bold
= _('Fingerprints')
.fingerprints-list.js-fingerprints-list{ data: { qa_selector: 'fingerprints_list' } }
diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml
index d5030a02cdd..0ab9d9c4005 100644
--- a/app/views/projects/no_repo.html.haml
+++ b/app/views/projects/no_repo.html.haml
@@ -22,4 +22,4 @@
- if can? current_user, :remove_project, @project
.prepend-top-20
- = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
+ = link_to _('Delete project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index d725098752d..66721a28e62 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -1,7 +1,7 @@
- access = note_max_access_for_user(note)
- if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR)
%span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
- = issuable_first_contribution_icon
+ = sprite_icon('first-contribution', css_class: 'gl-icon gl-vertical-align-top')
- if access.nonzero?
%span.note-role.user-access-role= Gitlab::Access.human_access(access)
diff --git a/app/views/projects/packages/packages/_legacy_package_list.html.haml b/app/views/projects/packages/packages/_legacy_package_list.html.haml
new file mode 100644
index 00000000000..43dbb5c3eee
--- /dev/null
+++ b/app/views/projects/packages/packages/_legacy_package_list.html.haml
@@ -0,0 +1,60 @@
+- sort_value = @sort
+- sort_title = packages_sort_option_title(sort_value)
+
+- if @packages.any?
+ .d-flex.justify-content-end
+ .dropdown.inline.gl-mt-3.gl-mb-3.package-sort-dropdown
+ .btn-group{ role: 'group' }
+ .btn-group{ role: 'group' }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
+ = sort_title
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
+ %li
+ = sortable_item(sort_title_created_date, package_sort_path(sort: sort_value_recently_created), sort_title)
+ = sortable_item(sort_title_name, package_sort_path(sort: sort_value_name_desc), sort_title)
+ = sortable_item(sort_title_version, package_sort_path(sort: sort_value_version_desc), sort_title)
+ = sortable_item(sort_title_type, package_sort_path(sort: sort_value_type_desc), sort_title)
+ = packages_sort_direction_button(sort_value)
+
+ .table-holder
+ .gl-responsive-table-row.table-row-header.bg-secondary-50.px-2.border-top{ role: 'row' }
+ .table-section.section-30{ role: 'rowheader' }
+ = _('Name')
+ .table-section.section-20{ role: 'rowheader' }
+ = _('Version')
+ .table-section.section-20{ role: 'rowheader' }
+ = _('Type')
+ .table-section.section-20{ role: 'rowheader' }
+ = _('Created')
+ .table-section.section-10{ role: 'rowheader' }
+ - @packages.each do |package|
+ .gl-responsive-table-row.package-row.px-2{ data: { qa_selector: "package_row" } }
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= _("Name")
+ .table-mobile-content.flex-truncate-parent
+ = link_to package.name, project_package_path(@project, package), class: 'flex-truncate-child', data: { qa_selector: "package_link" }
+ .table-section.section-20
+ .table-mobile-header{ role: "rowheader" }= _("Version")
+ .table-mobile-content
+ = package.version
+ .table-section.section-20
+ .table-mobile-header{ role: "rowheader" }= _("Type")
+ .table-mobile-content
+ = package.package_type
+ .table-section.section-20
+ .table-mobile-header{ role: "rowheader" }= _("Created")
+ .table-mobile-content
+ = time_ago_with_tooltip(package.created_at)
+ .table-section.section-10
+ .table-mobile-header{ role: "rowheader" }
+ .table-mobile-content
+ - if can_destroy_package
+ .float-right
+ = link_to project_package_path(@project, package), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-remove", title: _('Delete Package') do
+ = icon('trash')
+ = paginate @packages, theme: "gitlab"
+- else
+ .row.empty-state
+ .col-12
+ = render 'shared/packages/no_packages'
diff --git a/app/views/projects/packages/packages/index.html.haml b/app/views/projects/packages/packages/index.html.haml
new file mode 100644
index 00000000000..c81326f3760
--- /dev/null
+++ b/app/views/projects/packages/packages/index.html.haml
@@ -0,0 +1,5 @@
+- page_title _("Packages")
+
+.row
+ .col-12
+ #js-vue-packages-list{ data: packages_list_data('projects', @project) }
diff --git a/app/views/projects/packages/packages/show.html.haml b/app/views/projects/packages/packages/show.html.haml
new file mode 100644
index 00000000000..a66ae466d9d
--- /dev/null
+++ b/app/views/projects/packages/packages/show.html.haml
@@ -0,0 +1,25 @@
+- add_to_breadcrumbs _("Packages"), project_packages_path(@project)
+- add_to_breadcrumbs @package.name, project_packages_path(@project)
+- breadcrumb_title @package.version
+- page_title _("Packages")
+
+.row
+ .col-12
+ #js-vue-packages-detail{ data: { package: package_from_presenter(@package),
+ can_delete: can?(current_user, :destroy_package, @project).to_s,
+ destroy_path: project_package_path(@project, @package),
+ svg_path: image_path('illustrations/no-packages.svg'),
+ npm_path: package_registry_instance_url(:npm),
+ npm_help_path: help_page_path('user/packages/npm_registry/index'),
+ maven_path: package_registry_project_url(@project.id, :maven),
+ maven_help_path: help_page_path('user/packages/maven_repository/index'),
+ conan_path: package_registry_instance_url(:conan),
+ conan_help_path: help_page_path('user/packages/conan_repository/index'),
+ nuget_path: nuget_package_registry_url(@project.id),
+ nuget_help_path: help_page_path('user/packages/nuget_repository/index'),
+ pypi_path: pypi_registry_url(@project.id),
+ pypi_setup_path: package_registry_project_url(@project.id, :pypi),
+ pypi_help_path: help_page_path('user/packages/pypi_repository/index'),
+ composer_path: composer_registry_url(@project&.group&.id),
+ composer_help_path: help_page_path('user/packages/composer_repository/index'),
+ project_name: @project.name} }
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
index 08dcba2afd7..63dd7ca1def 100644
--- a/app/views/projects/pages/_access.html.haml
+++ b/app/views/projects/pages/_access.html.haml
@@ -18,6 +18,6 @@
- help_page = help_page_path('/user/project/pages/pages_access_control')
- link_start = '<a href="%{url}" target="_blank" class="alert-link" rel="noopener noreferrer">'.html_safe % { url: help_page }
- link_end = '</a>'.html_safe
- = s_('GitLabPages|Access Control is enabled for this Pages website; only authorized users will be able to access it. To make your website publicly available, navigate to your project\'s %{strong_start}Settings > General > Visibility%{strong_end} and select %{strong_start}Everyone%{strong_end} in pages section. Read the %{link_start}documentation%{link_end} for more information.').html_safe % { link_start: link_start, link_end: link_end, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = html_escape_once(s_('GitLabPages|Access Control is enabled for this Pages website; only authorized users will be able to access it. To make your website publicly available, navigate to your project\'s %{strong_start}Settings &gt; General &gt; Visibility%{strong_end} and select %{strong_start}Everyone%{strong_end} in pages section. Read the %{link_start}documentation%{link_end} for more information.')).html_safe % { link_start: link_start, link_end: link_end, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
.card-footer.alert-primary
= s_('GitLabPages|It may take up to 30 minutes before the site is available after the first deployment.')
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index c116efe521a..af6de10b2a0 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -10,7 +10,7 @@
- if verification_enabled
- tooltip, status = domain.unverified? ? [s_('GitLabPages|Unverified'), 'failed'] : [s_('GitLabPages|Verified'), 'success']
.domain-status.ci-status-icon.has-tooltip{ class: "ci-status-icon-#{status}", title: tooltip }
- = sprite_icon("status_#{status}", size: 16 )
+ = sprite_icon("status_#{status}" )
.domain-name
= external_link(domain.url, domain.url)
- if domain.certificate
diff --git a/app/views/projects/pages/_pages_settings.html.haml b/app/views/projects/pages/_pages_settings.html.haml
index 58eddf630f4..8aa02074205 100644
--- a/app/views/projects/pages/_pages_settings.html.haml
+++ b/app/views/projects/pages/_pages_settings.html.haml
@@ -1,4 +1,4 @@
-= form_for @project, url: namespace_project_pages_path(@project.namespace.becomes(Namespace), @project), html: { class: 'inline', title: pages_https_only_title } do |f|
+= form_for @project, url: project_pages_path(@project), html: { class: 'inline', title: pages_https_only_title } do |f|
- if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
= render_if_exists 'shared/pages/max_pages_size_input', form: f
@@ -9,5 +9,5 @@
%strong
= s_('GitLabPages|Force HTTPS (requires valid certificates)')
- .prepend-top-10
+ .gl-mt-3
= f.submit s_('GitLabPages|Save'), class: 'btn btn-success'
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index fc69b390bde..8a01945ffac 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -25,4 +25,4 @@
= render 'destroy'
- else
.bs-callout.bs-callout-warning
- = s_('GitLabPages|GitLab Pages are disabled for this project. You can enable them on your project\'s %{strong_start}Settings > General > Visibility%{strong_end} page.').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = html_escape_once(s_('GitLabPages|GitLab Pages are disabled for this project. You can enable them on your project\'s %{strong_start}Settings &gt; General &gt; Visibility%{strong_end} page.')).html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
diff --git a/app/views/projects/pages_domains/_certificate.html.haml b/app/views/projects/pages_domains/_certificate.html.haml
index 11c7e4a950b..16d949c416b 100644
--- a/app/views/projects/pages_domains/_certificate.html.haml
+++ b/app/views/projects/pages_domains/_certificate.html.haml
@@ -19,8 +19,8 @@
"aria-label": _("Automatic certificate management using Let's Encrypt") }
= f.hidden_field :auto_ssl_enabled?, class: "js-project-feature-toggle-input"
%span.toggle-icon
- = sprite_icon("status_success_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-checked")
- = sprite_icon("status_failed_borderless", size: 16, css_class: "toggle-icon-svg toggle-status-unchecked")
+ = sprite_icon("status_success_borderless", size: 18, css_class: "gl-text-blue-500 toggle-status-checked")
+ = sprite_icon("status_failed_borderless", size: 18, css_class: "gl-text-gray-400 toggle-status-unchecked")
%p.text-secondary.mt-3
- docs_link_url = help_page_path("user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md")
- docs_link_start = "<a href=\"%{docs_link_url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-nowrap\">".html_safe % { docs_link_url: docs_link_url }
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index f5dc3ccc60e..0c3ab4f10a6 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -4,7 +4,7 @@
= _("New Pages Domain")
= render 'projects/pages_domains/helper_text'
%div
- = form_for [@project.namespace.becomes(Namespace), @project, domain_presenter], html: { class: 'fieldset-form' } do |f|
+ = form_for [@project, domain_presenter], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions
= f.submit _('Create New Domain'), class: "btn btn-success"
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index e1be7335a3f..20ecf948447 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -14,7 +14,7 @@
= _('Pages Domain')
= render 'projects/pages_domains/helper_text'
%div
- = form_for [@project.namespace.becomes(Namespace), @project, domain_presenter], html: { class: 'fieldset-form' } do |f|
+ = form_for [@project, domain_presenter], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
.form-actions.d-flex.justify-content-between
= f.submit _('Save Changes'), class: "btn btn-success"
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index 20cf2ed63b5..1a8229350d9 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form pipeline-schedule-form" } do |f|
+= form_for [@project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "js-pipeline-schedule-form pipeline-schedule-form" } do |f|
= form_errors(@schedule)
.form-group.row
.col-md-9
@@ -27,7 +27,7 @@
= render 'ci/variables/variable_row', form_field: 'schedule', variable: variable, only_key_value: true
= render 'ci/variables/variable_row', form_field: 'schedule', only_key_value: true
- if @schedule.variables.size > 0
- %button.btn.btn-info.btn-inverted.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@schedule.variables.size == 0}" } }
+ %button.btn.btn-info.btn-inverted.gl-mt-3.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@schedule.variables.size == 0}" } }
- if @schedule.variables.size == 0
= n_('Hide value', 'Hide values', @schedule.variables.size)
- else
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index f48763cb544..ca71aa8a24d 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -33,8 +33,8 @@
= link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do
= s_('PipelineSchedules|Take ownership')
- if can?(current_user, :update_pipeline_schedule, pipeline_schedule)
- = link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn' do
- = icon('pencil')
+ = link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn gl-display-flex' do
+ = sprite_icon('pencil')
- if can?(current_user, :admin_pipeline_schedule, pipeline_schedule)
= link_to pipeline_schedule_path(pipeline_schedule), title: _('Delete'), method: :delete, class: 'btn btn-remove', data: { confirm: _("Are you sure you want to delete this pipeline schedule?") } do
- = icon('trash')
+ = sprite_icon('remove')
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 85902d51ab0..c54a19b8f61 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -7,8 +7,8 @@
.info-well
.well-segment.pipeline-info
- .icon-container
- = icon('clock-o')
+ .icon-container.gl-vertical-align-text-bottom
+ = sprite_icon('clock')
= pluralize @pipeline.total_size, "job"
= @pipeline.ref_text
- if @pipeline.duration
@@ -35,7 +35,7 @@
%span.js-pipeline-url-failure.badge.badge-danger.has-tooltip{ title: @pipeline.failure_reason }
error
- if @pipeline.auto_devops_source?
- - popover_title_text = _('This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>').html_safe
+ - popover_title_text = html_escape(_('This pipeline makes use of a predefined CI/CD configuration enabled by %{b_open}Auto DevOps.%{b_close}')) % { b_open: '<b>'.html_safe, b_close: '</b>'.html_safe }
- popover_content_url = help_page_path('topics/autodevops/index.md')
- popover_content_text = _('Learn more about Auto DevOps')
%a.js-pipeline-url-autodevops.badge.badge-info.autodevops-badge{ href: "#", tabindex: "0", role: "button", data: { container: "body",
diff --git a/app/views/projects/pipelines/_pipeline_warnings.html.haml b/app/views/projects/pipelines/_pipeline_warnings.html.haml
new file mode 100644
index 00000000000..e27bd440462
--- /dev/null
+++ b/app/views/projects/pipelines/_pipeline_warnings.html.haml
@@ -0,0 +1,6 @@
+- if warnings.any?
+ - warnings.map(&:content).each do |warning|
+ .bs-callout.bs-callout-warning
+ %p
+ %b= _("Warning:")
+ = markdown(warning)
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index be947b42e25..4ae06e1e16f 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,4 +1,4 @@
-- test_reports_enabled = Feature.enabled?(:junit_pipeline_view, @project)
+- return if pipeline_has_errors
- dag_pipeline_tab_enabled = Feature.enabled?(:dag_pipeline_tab, @project, default_enabled: true)
.tabs-holder
@@ -10,7 +10,6 @@
%li.js-dag-tab-link
= link_to dag_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-dag', action: 'dag', toggle: 'tab' }, class: 'dag-tab' do
= _('DAG')
- %span.badge-pill.gl-badge.sm.gl-bg-blue-500.gl-text-white.gl-ml-2= _('Beta')
%li.js-builds-tab-link
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
= _('Jobs')
@@ -20,11 +19,10 @@
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
= _('Failed Jobs')
%span.badge.badge-pill.js-failures-counter= @pipeline.failed_builds.count
- - if test_reports_enabled
- %li.js-tests-tab-link
- = link_to test_report_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-tests', action: 'test_report', toggle: 'tab' }, class: 'test-tab' do
- = s_('TestReports|Tests')
- %span.badge.badge-pill.js-test-report-badge-counter= Feature.enabled?(:build_report_summary, @project) ? @pipeline.test_report_summary.total_count : ''
+ %li.js-tests-tab-link
+ = link_to test_report_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-tests', action: 'test_report', toggle: 'tab' }, class: 'test-tab' do
+ = s_('TestReports|Tests')
+ %span.badge.badge-pill.js-test-report-badge-counter= @pipeline.test_report_summary.total[:count]
= render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
.tab-content
@@ -71,8 +69,8 @@
= build.present.callout_failure_message
%td.responsive-table-cell.build-actions
- if can?(current_user, :update_build, job)
- = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
- = icon('repeat')
+ = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build gl-button btn-icon btn-default' do
+ = sprite_icon('repeat', css_class: 'gl-icon')
- if can?(current_user, :read_build, job)
%tr.build-trace-row.responsive-table-border-end
%td
@@ -83,10 +81,9 @@
- if dag_pipeline_tab_enabled
#js-tab-dag.tab-pane
- #js-pipeline-dag-vue{ data: { pipeline_data_path: dag_project_pipeline_path(@project, @pipeline), empty_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), dag_doc_path: help_page_path('ci/yaml/README.md', anchor: 'needs')} }
+ #js-pipeline-dag-vue{ data: { pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, empty_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), dag_doc_path: help_page_path('ci/yaml/README.md', anchor: 'needs')} }
#js-tab-tests.tab-pane
- #js-pipeline-tests-detail{ data: { full_report_endpoint: test_report_project_pipeline_path(@project, @pipeline, format: :json),
- summary_endpoint: Feature.enabled?(:build_report_summary, @project) ? summary_project_pipeline_tests_path(@project, @pipeline, format: :json) : '',
- count_endpoint: test_reports_count_project_pipeline_path(@project, @pipeline, format: :json) } }
+ #js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json),
+ suite_endpoint: project_pipeline_test_path(@project, @pipeline, suite_name: ':suite_name', format: :json) } }
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index a3e46a0939c..726bf9af223 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -6,37 +6,42 @@
= s_('Pipeline|Run Pipeline')
%hr
-= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
- = form_errors(@pipeline)
- .form-group.row
- .col-sm-12
- = f.label :ref, s_('Pipeline|Run for'), class: 'col-form-label'
- = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
- = dropdown_tag(params[:ref] || @project.default_branch,
- options: { toggle_class: 'js-branch-select wide monospace',
- filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
- data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
- .form-text.text-muted
- = s_("Pipeline|Existing branch name or tag")
+- if Feature.enabled?(:new_pipeline_form)
+ #js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), ref_param: params[:ref] || @project.default_branch, var_param: params[:var].to_json, file_param: params[:file_var].to_json, ref_names: @project.repository.ref_names.to_json.html_safe, settings_link: project_settings_ci_cd_path(@project) } }
- .col-sm-12.prepend-top-10.js-ci-variable-list-section
- %label
- = s_('Pipeline|Variables')
- %ul.ci-variable-list
- - if params[:var]
- - params[:var].each do |variable|
- = render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable
- - if params[:file_var]
- - params[:file_var].each do |variable|
- - variable.push("file")
- = render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable
- = render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true
- .form-text.text-muted
- = (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
+- else
+ = form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|
+ = form_errors(@pipeline)
+ = pipeline_warnings(@pipeline)
+ .form-group.row
+ .col-sm-12
+ = f.label :ref, s_('Pipeline|Run for'), class: 'col-form-label'
+ = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
+ = dropdown_tag(params[:ref] || @project.default_branch,
+ options: { toggle_class: 'js-branch-select wide monospace',
+ filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"),
+ data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
+ .form-text.text-muted
+ = s_("Pipeline|Existing branch name or tag")
- .form-actions
- = f.submit s_('Pipeline|Run Pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
- = link_to _('Cancel'), project_pipelines_path(@project), class: 'btn btn-default float-right'
+ .col-sm-12.gl-mt-3.js-ci-variable-list-section
+ %label
+ = s_('Pipeline|Variables')
+ %ul.ci-variable-list
+ - if params[:var]
+ - params[:var].each do |variable|
+ = render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable
+ - if params[:file_var]
+ - params[:file_var].each do |variable|
+ - variable.push("file")
+ = render 'ci/variables/url_query_variable_row', form_field: 'pipeline', variable: variable
+ = render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true
+ .form-text.text-muted
+ = (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe
--# haml-lint:disable InlineJavaScript
-%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
+ .form-actions
+ = f.submit s_('Pipeline|Run Pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
+ = link_to _('Cancel'), project_pipelines_path(@project), class: 'btn btn-default float-right'
+
+ -# haml-lint:disable InlineJavaScript
+ %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 2b2133b8296..e1a606b1765 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -1,6 +1,7 @@
- add_to_breadcrumbs _('Pipelines'), project_pipelines_path(@project)
- breadcrumb_title "##{@pipeline.id}"
- page_title _('Pipeline')
+- pipeline_has_errors = @pipeline.builds.empty? && @pipeline.yaml_errors.present?
.js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } }
#js-pipeline-header-vue.pipeline-header-container
@@ -8,7 +9,7 @@
- if @pipeline.commit.present?
= render "projects/pipelines/info", commit: @pipeline.commit
- - if @pipeline.builds.empty? && @pipeline.yaml_errors.present?
+ - if pipeline_has_errors
.bs-callout.bs-callout-danger
%h4= _('Found errors in your %{gitlab_ci_yml}:') % { gitlab_ci_yml: '.gitlab-ci.yml' }
%ul
@@ -17,7 +18,8 @@
- lint_link_url = project_ci_lint_path(@project)
- lint_link_start = '<a href="%{url}">'.html_safe % { url: lint_link_url }
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
- - else
- = render "projects/pipelines/with_tabs", pipeline: @pipeline
+
+ = render "projects/pipelines/pipeline_warnings", warnings: @pipeline.warning_messages
+ = render "projects/pipelines/with_tabs", pipeline: @pipeline, pipeline_has_errors: pipeline_has_errors
.js-pipeline-details-vue{ data: { endpoint: project_pipeline_path(@project, @pipeline, format: :json) } }
diff --git a/app/views/projects/product_analytics/_graph.html.haml b/app/views/projects/product_analytics/_graph.html.haml
new file mode 100644
index 00000000000..fd81a248005
--- /dev/null
+++ b/app/views/projects/product_analytics/_graph.html.haml
@@ -0,0 +1,6 @@
+- graph = local_assigns.fetch(:graph)
+
+%h3
+ = graph[:id]
+
+.js-project-analytics-chart{ "data-chart-data": graph.to_json, "data-chart-id": graph[:id] }
diff --git a/app/views/projects/product_analytics/_links.html.haml b/app/views/projects/product_analytics/_links.html.haml
new file mode 100644
index 00000000000..0797c5baf91
--- /dev/null
+++ b/app/views/projects/product_analytics/_links.html.haml
@@ -0,0 +1,10 @@
+.mb-3
+ %ul.nav-links
+ = nav_link(path: 'product_analytics#index') do
+ = link_to _('Events'), project_product_analytics_path(@project)
+ = nav_link(path: 'product_analytics#graphs') do
+ = link_to 'Graphs', graphs_project_product_analytics_path(@project)
+ = nav_link(path: 'product_analytics#test') do
+ = link_to _('Test'), test_project_product_analytics_path(@project)
+ = nav_link(path: 'product_analytics#setup') do
+ = link_to _('Setup'), setup_project_product_analytics_path(@project)
diff --git a/app/views/projects/product_analytics/_tracker.html.erb b/app/views/projects/product_analytics/_tracker.html.erb
new file mode 100644
index 00000000000..dbb96f19e22
--- /dev/null
+++ b/app/views/projects/product_analytics/_tracker.html.erb
@@ -0,0 +1,10 @@
+;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[];
+p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments)
+};p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1;
+n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","<%= product_analytics_tracker_url -%>","snowplow<%= @random -%>"));
+snowplow<%= @random -%>("newTracker", "sp", "<%= product_analytics_tracker_collector_url -%>", {
+ appId: "<%= @project_id -%>",
+ platform: "<%= @platform -%>",
+ eventMethod: "get"
+});
+snowplow<%= @random -%>('trackPageView');
diff --git a/app/views/projects/product_analytics/graphs.html.haml b/app/views/projects/product_analytics/graphs.html.haml
new file mode 100644
index 00000000000..89286061594
--- /dev/null
+++ b/app/views/projects/product_analytics/graphs.html.haml
@@ -0,0 +1,12 @@
+- page_title _('Product Analytics')
+
+= render 'links'
+
+%p
+ = _('Showing graphs based on events of the last %{timerange} days.') % { timerange: @timerange }
+
+- @graphs.each_slice(2) do |pair|
+ .row.append-bottom-10
+ - pair.each do |graph|
+ .col-md-6{ id: graph[:id] }
+ = render 'graph', graph: graph
diff --git a/app/views/projects/product_analytics/index.html.haml b/app/views/projects/product_analytics/index.html.haml
new file mode 100644
index 00000000000..386f9265179
--- /dev/null
+++ b/app/views/projects/product_analytics/index.html.haml
@@ -0,0 +1,16 @@
+- page_title _('Product Analytics')
+
+= render 'links'
+
+- if @events.any?
+ %p
+ - if @events.total_count > @events.size
+ = _('Number of events for this project: %{total_count}.') % { total_count: number_with_delimiter(@events.total_count) }
+ %ol
+ - @events.each do |event|
+ %li
+ %code= event.as_json_wo_empty
+- else
+ .empty-state
+ .text-content
+ = _('There are currently no events.')
diff --git a/app/views/projects/product_analytics/setup.html.haml b/app/views/projects/product_analytics/setup.html.haml
new file mode 100644
index 00000000000..e1819c7d74b
--- /dev/null
+++ b/app/views/projects/product_analytics/setup.html.haml
@@ -0,0 +1,12 @@
+- page_title _('Product Analytics')
+
+= render 'links'
+
+%p
+ = _('Copy the code below to implement tracking in your application:')
+
+%pre
+ = render "tracker"
+
+%p.hint
+ = _('A platform value can be web, mob or app.')
diff --git a/app/views/projects/product_analytics/test.html.haml b/app/views/projects/product_analytics/test.html.haml
new file mode 100644
index 00000000000..60d897ee138
--- /dev/null
+++ b/app/views/projects/product_analytics/test.html.haml
@@ -0,0 +1,16 @@
+- page_title _('Product Analytics')
+
+= render 'links'
+
+%p
+ = _('This page sends a payload. Go back to the events page to see a newly created event.')
+
+- if @event
+ %p
+ = _('Last item before this page loaded in your browser:')
+
+ %code
+ = @event.as_json_wo_empty
+
+:javascript
+ #{render 'tracker'}
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
index 353c36d0fed..39ef1e52a0d 100644
--- a/app/views/projects/project_members/_groups.html.haml
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -1,6 +1,6 @@
.card.project-members-groups
.card-header
- = _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize(@project.name, tags: []) }
+ = html_escape(_("Groups with access to %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(@project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%span.badge.badge-pill= group_links.size
%ul.content-list.members-list
- can_admin_member = can?(current_user, :admin_project_member, @project)
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 5d8005b2e2a..4b3fdf8d0b1 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -4,7 +4,7 @@
.card
.card-header.flex-project-members-panel
%span.flex-project-title
- = _("Members of <strong>%{project_name}</strong>").html_safe % { project_name: sanitize(project.name, tags: []) }
+ = html_escape(_("Members of %{strong_open}%{project_name}%{strong_close}")) % { project_name: sanitize(project.name, tags: []), strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%span.badge.badge-pill= members.total_count
= form_tag project_project_members_path(project), method: :get, class: 'form-inline user-search-form flex-users-form' do
.form-group
@@ -12,6 +12,7 @@
= search_field_tag :search, params[:search], { placeholder: _('Find existing members by name'), class: 'form-control', spellcheck: false }
%button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") }
= icon("search")
+ = label_tag :sort_by, _('Sort by'), class: 'col-form-label label-bold px-2'
= render 'shared/members/sort_dropdown'
%ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
= render partial: 'shared/members/member', collection: members, as: :member
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index ba964e5cd37..9a1e997fce7 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -11,7 +11,7 @@
%p= share_project_description(@project)
- else
%p
- = _("Members can be added by project <i>Maintainers</i> or <i>Owners</i>").html_safe
+ = html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
.light
- if can_admin_project_members && project_can_be_shared?
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
index 74bfaa9ff80..b2ec98be056 100644
--- a/app/views/projects/protected_branches/_update_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml
@@ -1,10 +1 @@
-%td
- = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level
- = dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
- data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }})
-%td
- = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
- = dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
- data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
+= render 'shared/projects/protected_branches/update_protected_branch', protected_branch: protected_branch
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 f84c7b39733..7131e9925b3 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.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'new-protected-branch js-new-protected-branch' } do |f|
+= 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' }
.card
.card-header
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index 63748d8d85f..f27936703de 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = expanded_by_default?
-%section.qa-protected-branches-settings.settings.no-animate#js-protected-branches-settings{ class: ('expanded' if expanded) }
+%section.settings.no-animate#js-protected-branches-settings{ class: ('expanded' if expanded), data: { qa_selector: 'protected_branches_settings_content' } }
.settings-header
%h4
Protected Branches
diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
index 4ca6ebe9c78..d62e9513d56 100644
--- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml
@@ -20,4 +20,4 @@
- if can_admin_project
%td
- = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch, { update_section: 'js-protected-branches-settings' }], disabled: local_assigns[:disabled], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning"
+ = link_to 'Unprotect', [@project, protected_branch, { update_section: 'js-protected-branches-settings' }], disabled: local_assigns[:disabled], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning"
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 8a6ae53a7c4..dc7514badb6 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,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f|
+= 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
@@ -24,5 +24,5 @@
.create_access_levels-container
= yield :create_access_levels
- .card-footer
- = f.submit 'Protect', class: 'btn-success btn', disabled: true, data: { qa_selector: 'protect_tag_button' }
+ .card-footer.gl-display-flex.gl-justify-content-end
+ = f.submit _('Protect'), class: 'btn-success btn', disabled: true, data: { qa_selector: 'protect_tag_button' }
diff --git a/app/views/projects/protected_tags/shared/_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_protected_tag.html.haml
index b0563163c9c..71c29f9b7b6 100644
--- a/app/views/projects/protected_tags/shared/_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_protected_tag.html.haml
@@ -19,4 +19,4 @@
- if can? current_user, :admin_project, @project
%td
- = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag, { update_section: 'js-protected-tags-settings' }], data: { confirm: 'Tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
+ = link_to 'Unprotect', [@project, protected_tag, { update_section: 'js-protected-tags-settings' }], data: { confirm: 'Tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 92680a70da2..74b6e981c00 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -6,11 +6,12 @@
= link_to _("%{token}...") % { token: runner.short_sha }, project_runner_path(@project, runner), class: 'commit-sha has-tooltip', title: _("Partial token for reference only")
- if runner.locked?
- = icon('lock', class: 'has-tooltip', title: _('Locked to current projects'))
+ %span.has-tooltip{ title: _('Locked to current projects') }
+ = sprite_icon('lock')
%small.edit-runner
- = link_to edit_project_runner_path(@project, runner) do
- %i.fa.fa-edit.btn
+ = link_to edit_project_runner_path(@project, runner), class: 'btn btn-edit' do
+ = sprite_icon('pencil')
- else
%span.commit-sha
= runner.short_sha
@@ -27,7 +28,7 @@
- runner_project = @project.runner_projects.find_by(runner_id: runner) # rubocop: disable CodeReuse/ActiveRecord
= link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- elsif runner.project_type?
- = form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
+ = form_for [@project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
= f.submit _('Enable for this project'), class: 'btn btn-sm'
.float-right
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 080b2c0b0e9..8a17ca3c670 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -15,7 +15,7 @@
= _('Enable shared Runners')
&nbsp; for this project
-- if @shared_runners_count.zero?
+- 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}
diff --git a/app/views/projects/serverless/functions/index.html.haml b/app/views/projects/serverless/functions/index.html.haml
index b21965915a2..383c187b398 100644
--- a/app/views/projects/serverless/functions/index.html.haml
+++ b/app/views/projects/serverless/functions/index.html.haml
@@ -7,7 +7,8 @@
.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path,
installed: @installed,
clusters_path: clusters_path,
- help_path: help_page_path('user/project/clusters/serverless/index') } }
+ help_path: help_page_path('user/project/clusters/serverless/index'),
+ empty_image_path: image_path('illustrations/empty-state/empty-serverless-lg.svg') } }
%div{ class: [('limit-container-width' unless fluid_layout)] }
.js-serverless-survey-banner{ data: { user_name: current_user.name, user_email: current_user.email } }
@@ -15,5 +16,5 @@
.js-serverless-functions-notice
.flash-container
- .top-area.adjust.d-flex.justify-content-center
+ .top-area.adjust.d-flex.justify-content-center.gl-border-none
.serverless-functions-table#js-serverless-functions
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 2e49e74a9b3..24b47f6e4b6 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -3,7 +3,7 @@
.row.gl-mt-3.gl-mb-3
.col-lg-4
- %h4.gl-mt-0
+ %h3.page-title.gl-mt-0
= @service.title
- [true, false].each do |value|
- hide_class = 'd-none' if @service.operating? != value
diff --git a/app/views/projects/services/alerts/_top.html.haml b/app/views/projects/services/alerts/_top.html.haml
index ebc93978832..e3bcb6bd3a0 100644
--- a/app/views/projects/services/alerts/_top.html.haml
+++ b/app/views/projects/services/alerts/_top.html.haml
@@ -1,7 +1,7 @@
.row
.col-lg-12
- .gl-alert.gl-alert-info.js-alerts-moved-alert{ role: 'alert' }
- = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert.gl-alert-info{ role: 'alert' }
+ = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= _('You can now manage alert endpoint configuration in the Alerts section on the Operations settings page. Fields on this page have been deprecated.')
.gl-alert-actions
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index cf73a7055c6..9d81fda68cb 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -1,5 +1,5 @@
-- pretty_name = @project&.full_name || _('<project name>')
-- run_actions_text = s_("ProjectService|Perform common operations on GitLab project: %{project_name}") % { project_name: pretty_name }
+- pretty_name = html_escape(@project&.full_name) || html_escape_once(_('&lt;project name&gt;')).html_safe
+- run_actions_text = html_escape(s_("ProjectService|Perform common operations on GitLab project: %{project_name}")) % { project_name: pretty_name }
%p= s_("ProjectService|To set up this service:")
%ul.list-unstyled.indent-list
@@ -7,13 +7,13 @@
1.
= link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do
Enable custom slash commands
- = sprite_icon('external-link', size: 16)
+ = sprite_icon('external-link')
on your Mattermost installation
%li
2.
= link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noopener noreferrer nofollow' do
Add a slash command
- = sprite_icon('external-link', size: 16)
+ = sprite_icon('external-link')
in your Mattermost team with these options:
%hr
@@ -21,7 +21,7 @@
.form-group
= label_tag :display_name, _('Display name'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
- = text_field_tag :display_name, "GitLab / #{pretty_name}", class: 'form-control form-control-sm', readonly: 'readonly'
+ = text_field_tag :display_name, "GitLab / #{pretty_name}".html_safe, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#display_name', class: 'input-group-text')
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index cc005dd69b7..1005d9f7990 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -6,7 +6,7 @@
= s_("MattermostService|This service allows users to perform common operations on this project by entering slash commands in Mattermost.")
= link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
= _("View documentation")
- = sprite_icon('external-link', size: 16)
+ = sprite_icon('external-link')
%p.inline
= s_("MattermostService|See list of available commands in Mattermost after setting up this service, by entering")
%kbd.inline /&lt;trigger&gt; help
diff --git a/app/views/projects/services/prometheus/_metrics.html.haml b/app/views/projects/services/prometheus/_metrics.html.haml
index 9f5160f3dd5..79f5e846bd7 100644
--- a/app/views/projects/services/prometheus/_metrics.html.haml
+++ b/app/views/projects/services/prometheus/_metrics.html.haml
@@ -5,7 +5,7 @@
.col-lg-3
%p
= s_('PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters.')
- = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus_library/index'), target: '_blank', rel: "noopener noreferrer"
+ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus'), target: '_blank', rel: "noopener noreferrer"
.col-lg-9
.card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/index') } }
diff --git a/app/views/projects/services/prometheus/_top.html.haml b/app/views/projects/services/prometheus/_top.html.haml
index 338414be5ab..0238a45b75f 100644
--- a/app/views/projects/services/prometheus/_top.html.haml
+++ b/app/views/projects/services/prometheus/_top.html.haml
@@ -2,8 +2,8 @@
.row
.col-lg-12
- .gl-alert.gl-alert-info.js-alerts-moved-alert{ role: 'alert' }
- = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert.gl-alert-info{ role: 'alert' }
+ = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
= s_('AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated.')
.gl-alert-actions
diff --git a/app/views/projects/services/slack/_help.haml b/app/views/projects/services/slack/_help.haml
index d7ea1b270f5..1fd448020a0 100644
--- a/app/views/projects/services/slack/_help.haml
+++ b/app/views/projects/services/slack/_help.haml
@@ -3,14 +3,14 @@
.info-well
.well-segment
- %p= s_('SlackIntegration|This service send notifications about projects\' events to Slack channels. To set up this service:')
+ %p= s_('SlackIntegration|This service sends notifications about project events to Slack channels. To set up this service:')
%ol
%li
- = s_('SlackIntegration|%{webhooks_link_start}Add an incoming webhook%{webhooks_link_end} in your Slack team. The default channel can be overridden for each event.').html_safe % { webhooks_link_start: webhooks_link_start, webhooks_link_end: '</a>'.html_safe }
+ = html_escape(s_('SlackIntegration|%{webhooks_link_start}Add an incoming webhook%{webhooks_link_end} in your Slack team. The default channel can be overridden for each event.')) % { webhooks_link_start: webhooks_link_start.html_safe, webhooks_link_end: '</a>'.html_safe }
%li
- = s_('SlackIntegration|Paste the <strong>Webhook URL</strong> into the field below.').html_safe
+ = html_escape(s_('SlackIntegration|Paste the %{strong_open}Webhook URL%{strong_close} into the field below.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%li
- = s_('SlackIntegration|Select events below to enable notifications. The <strong>Slack channel names</strong> and <strong>Slack username</strong> fields are optional.').html_safe
+ = html_escape(s_('SlackIntegration|Select events below to enable notifications. The %{strong_open}Slack channel names%{strong_close} and %{strong_open}Slack username%{strong_close} fields are optional.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%p.mt-3.mb-0
- = s_('SlackIntegration|<strong>Note:</strong> Usernames and private channels are not supported.').html_safe
+ = html_escape(s_('SlackIntegration|%{strong_open}Note:%{strong_close} Usernames and private channels are not supported.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
= link_to _('Learn more'), help_page_path('user/project/integrations/slack')
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 0cf78d4f681..86486d95eb7 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,5 +1,5 @@
-- pretty_name = @project&.full_name || _('<project name>')
-- run_actions_text = s_("ProjectService|Perform common operations on GitLab project: %{project_name}") % { project_name: pretty_name }
+- pretty_name = @project&.full_name || _('&lt;project name&gt;')
+- run_actions_text = html_escape_once(s_("ProjectService|Perform common operations on GitLab project: %{project_name}") % { project_name: pretty_name })
.info-well
.well-segment
@@ -7,7 +7,7 @@
= s_("SlackService|This service allows users to perform common operations on this project by entering slash commands in Slack.")
= link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
= _("View documentation")
- = sprite_icon('external-link', size: 16)
+ = sprite_icon('external-link')
%p.inline
= s_("SlackService|See list of available commands in Slack after setting up this service, by entering")
%kbd.inline /&lt;command&gt; help
@@ -18,7 +18,7 @@
1.
= link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
Add a slash command
- = sprite_icon('external-link', size: 16)
+ = sprite_icon('external-link')
in your Slack team with these options:
%hr
@@ -67,7 +67,7 @@
.form-group
= label_tag :autocomplete_description, _('Autocomplete description'), class: 'col-12 col-form-label label-bold'
.col-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
+ = text_field_tag :autocomplete_description, run_actions_text.html_safe, class: 'form-control form-control-sm', readonly: 'readonly'
.input-group-append
= clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
@@ -89,6 +89,6 @@
%ul.list-unstyled.indent-list
%li
- = s_("SlackService|2. Paste the <strong>Token</strong> into the field below").html_safe
+ = html_escape(s_("SlackService|2. Paste the %{strong_open}Token%{strong_close} into the field below")) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
%li
- = s_("SlackService|3. Select the <strong>Active</strong> checkbox, press <strong>Save changes</strong> and start using GitLab inside Slack!").html_safe
+ = html_escape(s_("SlackService|3. Select the %{strong_open}Active%{strong_close} checkbox, press %{strong_open}Save changes%{strong_close} and start using GitLab inside Slack!")) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
diff --git a/app/views/projects/settings/_archive.html.haml b/app/views/projects/settings/_archive.html.haml
index 3307c3775ec..cbeedbd080c 100644
--- a/app/views/projects/settings/_archive.html.haml
+++ b/app/views/projects/settings/_archive.html.haml
@@ -1,6 +1,6 @@
- return unless can?(current_user, :archive_project, @project)
-.sub-section
+.sub-section{ data: { qa_selector: 'archive_project_content' } }
%h4.warning-title
- if @project.archived?
= _('Unarchive project')
@@ -13,6 +13,7 @@
method: :post, class: "btn btn-success"
- else
%p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- = link_to _('Archive project'), archive_project_path(@project),
+ .gl-display-flex.gl-justify-content-end
+ = link_to _('Archive project'), archive_project_path(@project),
data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
method: :post, class: "btn btn-warning"
diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml
index 3ffa029a25d..50f80fd1e2f 100644
--- a/app/views/projects/settings/_general.html.haml
+++ b/app/views/projects/settings/_general.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f|
+= form_for [@project], remote: true, html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-general-settings' }
= form_errors(@project)
@@ -31,7 +31,7 @@
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project
- .form-group.gl-mt-3.append-bottom-20
+ .form-group.gl-mt-3.gl-mb-3
.avatar-container.s90
= project_icon(@project, alt: _('Project avatar'), class: 'avatar project-avatar s90')
= f.label :avatar, _('Project avatar'), class: 'label-bold d-block'
@@ -40,5 +40,5 @@
%hr
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link'
-
- = f.submit _('Save changes'), class: "btn btn-success mt-4 qa-save-naming-topics-avatar-button"
+ .gl-display-flex.gl-justify-content-end
+ = f.submit _('Save changes'), class: "btn btn-success mt-4 qa-save-naming-topics-avatar-button"
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
index 4992288a8c8..100eb5991dc 100644
--- a/app/views/projects/settings/access_tokens/index.html.haml
+++ b/app/views/projects/settings/access_tokens/index.html.haml
@@ -10,8 +10,9 @@
= page_title
%p
= _('You can generate an access token scoped to this project for each application to use the GitLab API.')
- %p
- = _('You can also use project access tokens to authenticate against Git over HTTP.')
+ -# Commented out until https://gitlab.com/gitlab-org/gitlab/-/issues/219551 is fixed
+ -# %p
+ -# = _('You can also use project access tokens to authenticate against Git over HTTP.')
.col-lg-8
- if @new_project_access_token
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 7284b4bb55d..b4c9e51f53a 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -34,7 +34,7 @@
- elsif !has_base_domain
%p.settings-message.text-center
= s_('CICD|You must add a %{base_domain_link_start}base domain%{link_end} to your %{kubernetes_cluster_link_start}Kubernetes cluster%{link_end} in order for your deployment strategy to work.').html_safe % { base_domain_link_start: base_domain_link_start, kubernetes_cluster_link_start: kubernetes_cluster_link_start, link_end: link_end }
- %label.prepend-top-10
+ %label.gl-mt-3
%strong= s_('CICD|Deployment strategy')
.form-check
= form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input'
@@ -54,4 +54,4 @@
= s_('CICD|Automatic deployment to staging, manual deployment to production')
= link_to icon('question-circle'), help_page_path('topics/autodevops/customize.md', anchor: 'incremental-rollout-to-production-premium'), target: '_blank'
- = f.submit _('Save changes'), class: "btn btn-success prepend-top-15", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), class: "btn btn-success gl-mt-5", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index e8e5a5f0256..a0dd06e3304 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -7,7 +7,7 @@
%h5.gl-mt-0
= _("Git strategy for pipelines")
%p
- = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code").html_safe
+ = html_escape(_("Choose between %{code_open}clone%{code_close} or %{code_open}fetch%{code_close} to get the recent application code")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to icon('question-circle'), help_page_path('ci/pipelines/settings', anchor: 'git-strategy'), target: '_blank'
.form-check
= f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' }
@@ -54,7 +54,7 @@
= f.label :ci_config_path, _('Custom CI configuration path'), class: 'label-bold'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.form-text.text-muted
- = _("The path to the CI configuration file. Defaults to <code>.gitlab-ci.yml</code>").html_safe
+ = html_escape(_("The path to the CI configuration file. Defaults to %{code_open}.gitlab-ci.yml%{code_close}")) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to icon('question-circle'), help_page_path('ci/pipelines/settings', anchor: 'custom-ci-configuration-path'), target: '_blank'
%hr
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index b5452fcca55..8e3be5fa086 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -74,3 +74,22 @@
= link_to _('More information'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy', target: '_blank', rel: 'noopener noreferrer')
.settings-content
= render 'projects/registry/settings/index'
+
+- if can?(current_user, :create_freeze_period, @project)
+ %section.settings.no-animate#js-deploy-freeze-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _("Deploy freezes")
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ - freeze_period_docs = help_page_path('user/project/releases/index', anchor: 'prevent-unintentional-releases-by-setting-a-deploy-freeze')
+ - freeze_period_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: freeze_period_docs }
+ = html_escape(s_('DeployFreeze|Specify times when deployments are not allowed for an environment. The %{filename} file must be updated to make deployment jobs aware of the %{freeze_period_link_start}freeze period%{freeze_period_link_end}.')) % { freeze_period_link_start: freeze_period_link_start, freeze_period_link_end: '</a>'.html_safe, filename: tag.code('gitlab-ci.yml') }
+
+ - cron_syntax_url = 'https://crontab.guru/'
+ - cron_syntax_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: cron_syntax_url }
+ = s_('DeployFreeze|You can specify deploy freezes using only %{cron_syntax_link_start}cron syntax%{cron_syntax_link_end}.').html_safe % { cron_syntax_link_start: cron_syntax_link_start, cron_syntax_link_end: "</a>".html_safe }
+
+ .settings-content
+ = render 'ci/deploy_freeze/index'
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index d9068bde847..18c6cb31874 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -4,9 +4,9 @@
- if show_webhooks_moved_alert?
.gl-alert.gl-alert-info.js-webhooks-moved-alert.gl-mt-3{ role: 'alert', data: { feature_id: UserCalloutsHelper::WEBHOOKS_MOVED, dismiss_endpoint: user_callouts_path } }
- = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
%button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ = sprite_icon('close', css_class: 'gl-icon')
.gl-alert-body
= _('Webhooks have moved. They can now be found under the Settings menu.')
.gl-alert-actions
diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml
index 393b1f9d21a..62b344b38f1 100644
--- a/app/views/projects/settings/operations/_error_tracking.html.haml
+++ b/app/views/projects/settings/operations/_error_tracking.html.haml
@@ -5,12 +5,12 @@
%section.settings.no-animate.js-error-tracking-settings
.settings-header
%h3{ :class => "h4" }
- = _('Error Tracking')
+ = _('Error tracking')
%button.btn.js-settings-toggle{ type: 'button' }
= _('Expand')
%p
= _('To link Sentry to GitLab, enter your Sentry URL and Auth Token.')
- = link_to _('More information'), help_page_path('user/project/operations/error_tracking'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('More information'), help_page_path('operations/error_tracking'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
.js-error-tracking-form{ data: { list_projects_endpoint: project_error_tracking_projects_path(@project, format: :json),
operations_settings_endpoint: project_settings_operations_path(@project),
diff --git a/app/views/projects/settings/operations/_metrics_dashboard.html.haml b/app/views/projects/settings/operations/_metrics_dashboard.html.haml
index edbada8444a..056d3e8102b 100644
--- a/app/views/projects/settings/operations/_metrics_dashboard.html.haml
+++ b/app/views/projects/settings/operations/_metrics_dashboard.html.haml
@@ -1,5 +1,5 @@
.js-operation-settings{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
- help_page: help_page_path('user/project/operations/dashboard_settings'),
+ help_page: help_page_path('operations/metrics/dashboards/settings'),
external_dashboard: { url: metrics_external_dashboard_url,
- help_page: help_page_path('user/project/operations/linking_to_an_external_dashboard') },
+ help_page: help_page_path('operations/metrics/dashboards/settings') },
dashboard_timezone: { setting: metrics_dashboard_timezone.upcase } } }
diff --git a/app/views/projects/snippets/verify.html.haml b/app/views/projects/snippets/verify.html.haml
index eb56f03b3f4..3c4f08e1df7 100644
--- a/app/views/projects/snippets/verify.html.haml
+++ b/app/views/projects/snippets/verify.html.haml
@@ -1,4 +1,2 @@
-- form = [@project.namespace.becomes(Namespace), @project, @snippet.becomes(Snippet)]
-
-= render 'layouts/recaptcha_verification', spammable: @snippet, form: form
+= render 'layouts/recaptcha_verification', spammable: @snippet
diff --git a/app/views/projects/starrers/index.html.haml b/app/views/projects/starrers/index.html.haml
index e55ed99f643..97996562e2c 100644
--- a/app/views/projects/starrers/index.html.haml
+++ b/app/views/projects/starrers/index.html.haml
@@ -22,7 +22,7 @@
= link_to filter_starrer_path(sort: value), class: ("is-active" if @sort == value) do
= title
- if @starrers.size > 0
- .row.prepend-top-10
+ .row.gl-mt-3
= render partial: 'starrer', collection: @starrers, as: :starrer
= paginate @starrers, theme: 'gitlab'
- else
diff --git a/app/views/projects/static_site_editor/show.html.haml b/app/views/projects/static_site_editor/show.html.haml
index 8d2649be588..2d817912335 100644
--- a/app/views/projects/static_site_editor/show.html.haml
+++ b/app/views/projects/static_site_editor/show.html.haml
@@ -1 +1 @@
-#static-site-editor{ data: @config.payload }
+#static-site-editor{ data: @config.payload.merge({ merge_requests_illustration_path: image_path('illustrations/merge_requests.svg') }) }
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 59c7d0401d1..c8a6168edfc 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -37,6 +37,6 @@
- if can?(current_user, :admin_tag, @project)
= link_to edit_project_tag_release_path(@project, tag.name), class: 'btn btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do
- = icon("pencil")
+ = sprite_icon("pencil")
= link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip gl-ml-3 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
= icon("trash-o")
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index e3d3f2226a8..e0def8cf155 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -26,8 +26,8 @@
- if can?(current_user, :admin_tag, @project)
= link_to new_project_tag_path(@project), class: 'btn btn-success new-tag-btn', data: { qa_selector: "new_tag_button" } do
= s_('TagsPage|New tag')
- = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn d-none d-sm-inline-block has-tooltip' do
- = icon("rss")
+ = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn btn-svg d-none d-sm-inline-block has-tooltip' do
+ = sprite_icon('rss', css_class: 'qa-rss-icon')
= render_if_exists 'projects/commits/mirror_status'
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index edb0577cebd..ff973e2922f 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -42,18 +42,18 @@
- if @tag.has_signature?
= render partial: 'projects/commit/signature', object: @tag.signature
- if can?(current_user, :admin_tag, @project)
- = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn btn-edit controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
- = icon("pencil")
- = link_to project_tree_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse files') do
- = sprite_icon('folder-open')
- = link_to project_commits_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse commits') do
- = icon('history')
+ = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn btn-icon btn-edit gl-button controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
+ = sprite_icon("pencil", css_class: 'gl-icon')
+ = link_to project_tree_path(@project, @tag.name), class: 'btn btn-icon gl-button controls-item has-tooltip', title: s_('TagsPage|Browse files') do
+ = sprite_icon('folder-open', css_class: 'gl-icon')
+ = link_to project_commits_path(@project, @tag.name), class: 'btn btn-icon gl-button controls-item has-tooltip', title: s_('TagsPage|Browse commits') do
+ = sprite_icon('history', css_class: 'gl-icon')
.btn-container.controls-item
= render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_tag, @project)
.btn-container.controls-item-full
- = link_to project_tag_path(@project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: @tag.name } } do
- %i.fa.fa-trash-o
+ = link_to project_tag_path(@project, @tag.name), class: "btn btn-icon btn-danger gl-button remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: @tag.name } } do
+ = sprite_icon('remove', css_class: 'gl-icon')
- if @tag.message.present?
%pre.wrap{ data: { qa_selector: 'tag_message_content' } }
diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml
index a9abfac239c..dec71cdb56a 100644
--- a/app/views/projects/triggers/_form.html.haml
+++ b/app/views/projects/triggers/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @trigger], html: { class: 'gl-show-field-errors' } do |f|
+= form_for [@project, @trigger], html: { class: 'gl-show-field-errors' } do |f|
= form_errors(@trigger)
- if @trigger.token
diff --git a/app/views/registrations/welcome.html.haml b/app/views/registrations/welcome.html.haml
index bc8d7ed10ef..ef3e0b1b4c0 100644
--- a/app/views/registrations/welcome.html.haml
+++ b/app/views/registrations/welcome.html.haml
@@ -1,6 +1,6 @@
- content_for(:page_title, _('Welcome to GitLab %{name}!') % { name: current_user.name })
.text-center.mb-3
- = _('In order to tailor your experience with GitLab we<br>would like to know a bit more about you.').html_safe
+ = html_escape(_('In order to tailor your experience with GitLab we%{br_tag}would like to know a bit more about you.')) % { br_tag: '<br/>'.html_safe }
.signup-box.p-3.mb-2
.signup-body
= form_for(current_user, url: users_sign_up_update_registration_path, html: { class: 'new_new_user gl-show-field-errors', 'aria-live' => 'assertive' }) do |f|
diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml
index dc75918eb93..b29707d391d 100644
--- a/app/views/search/_form.html.haml
+++ b/app/views/search/_form.html.haml
@@ -11,7 +11,7 @@
= search_field_tag :search, params[:search], placeholder: _("Search for projects, issues, etc."), class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false
= icon("search", class: "search-icon")
%button.search-clear.js-search-clear{ class: ("hidden" if !params[:search].present?), type: "button", tabindex: "-1" }
- = icon("times-circle")
+ = sprite_icon('clear')
%span.sr-only
= _("Clear search")
- unless params[:snippets].eql? 'true'
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 8ada8c875f7..79f01c61833 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -8,7 +8,7 @@
= search_entries_info(@search_objects, @scope, @search_term)
- unless @show_snippets
- if @project
- - link_to_project = link_to(@project.full_name, [@project.namespace.becomes(Namespace), @project], class: 'ml-md-1')
+ - link_to_project = link_to(@project.full_name, @project, class: 'ml-md-1')
- if @scope == 'blobs'
- repository_ref = params[:repository_ref].to_s.presence || @project.default_branch
= s_("SearchCodeResults|in")
@@ -22,7 +22,7 @@
= _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group }
= render_if_exists 'shared/promotions/promote_advanced_search'
- .results.prepend-top-10
+ .results.gl-mt-3
- if @scope == 'commits'
%ul.content-list.commit-list
= render partial: "search/results/commit", collection: @search_objects
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index b88e9a75053..2f6024c3f2b 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -1,7 +1,7 @@
.search-result-row
%h4
= confidential_icon(issue)
- = link_to namespace_project_issue_path(issue.project.namespace.becomes(Namespace), issue.project, issue) do
+ = link_to project_issue_path(issue.project, issue) do
%span.term.str-truncated= issue.title
- if issue.closed?
%span.badge.badge-danger.gl-ml-2= _("Closed")
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 45b6cb06753..680c2ea0208 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -1,6 +1,6 @@
.search-result-row
%h4
- = link_to namespace_project_merge_request_path(merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request) do
+ = link_to project_merge_request_path(merge_request.target_project, merge_request) do
%span.term.str-truncated= merge_request.title
- if merge_request.merged?
%span.badge.badge-primary.gl-ml-2= _("Merged")
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index 3201f1a7815..53c2d380bc5 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -1,6 +1,6 @@
.search-result-row
%h4
- = link_to namespace_project_milestone_path(milestone.project.namespace.becomes(Namespace), milestone.project, milestone) do
+ = link_to project_milestone_path(milestone.project, milestone) do
%span.term.str-truncated= milestone.title
- if milestone.description.present?
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index b67bc71941a..a83b003a516 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -4,7 +4,7 @@
.search-result-row
%h5.note-search-caption.str-truncated
- = sprite_icon('comment', size: 16, css_class: 'gl-vertical-align-text-bottom')
+ = sprite_icon('comment', css_class: 'gl-vertical-align-text-bottom')
= link_to_member(project, note.author, avatar: false)
- link_to_project = link_to(project.full_name, project)
= _("commented on %{link_to_project}").html_safe % { link_to_project: link_to_project }
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index 869890cdf31..18eaccb46b2 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -2,6 +2,10 @@
- page_title @search_term
- @hide_breadcrumbs = true
+- if @search_results
+ - page_description(_("%{count} %{scope} for term '%{term}'") % { count: @search_results.formatted_count(@scope), scope: @scope, term: @search_term })
+ - page_card_attributes("Namespace" => @group&.full_path, "Project" => @project&.full_path)
+
.page-title-holder.d-sm-flex.align-items-sm-center
%h1.page-title<
= _('Search')
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
index 7aeecf26c39..2ad29707c9f 100644
--- a/app/views/sent_notifications/unsubscribe.html.haml
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -3,7 +3,7 @@
- noteable_text = show_unsubscribe_title?(noteable) ? %(#{noteable.title} (#{noteable.to_reference})) : %(#{noteable.to_reference})
- show_project_path = can_read_project?(@sent_notification.project)
- project_path = show_project_path ? @sent_notification.project.full_name : _("GitLab / Unsubscribe")
-- noteable_url = show_project_path ? url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable]) : breadcrumb_title_link
+- noteable_url = show_project_path ? url_for([@sent_notification.project, noteable]) : breadcrumb_title_link
- page_title _('Unsubscribe'), noteable_text, noteable_type.pluralize, project_path
%h3.page-title
diff --git a/app/views/shared/_broadcast_message.html.haml b/app/views/shared/_broadcast_message.html.haml
index 3e889900981..e313946a968 100644
--- a/app/views/shared/_broadcast_message.html.haml
+++ b/app/views/shared/_broadcast_message.html.haml
@@ -3,7 +3,7 @@
%div{ class: "broadcast-message #{'alert-warning' if is_banner} broadcast-#{message.broadcast_type}-message #{opts[:preview] && 'preview'} js-broadcast-notification-#{message.id} gl-display-flex",
style: broadcast_message_style(message), dir: 'auto' }
.flex-grow-1.text-right.pr-2
- = sprite_icon('bullhorn', size: 16, css_class: 'vertical-align-text-top')
+ = sprite_icon('bullhorn', css_class: 'vertical-align-text-top')
%div{ class: !fluid_layout && 'container-limited' }
= render_broadcast_message(message)
.flex-grow-1.text-right{ style: 'flex-basis: 0' }
diff --git a/app/views/shared/_check_recovery_settings.html.haml b/app/views/shared/_check_recovery_settings.html.haml
index e3de34a5ab9..7ac90e5af03 100644
--- a/app/views/shared/_check_recovery_settings.html.haml
+++ b/app/views/shared/_check_recovery_settings.html.haml
@@ -1,6 +1,6 @@
.gl-alert.gl-alert-warning.js-recovery-settings-callout{ role: 'alert', data: { feature_id: "account_recovery_regular_check", dismiss_endpoint: user_callouts_path, defer_links: "true" } }
%button.js-close.gl-alert-dismiss.gl-cursor-pointer{ type: 'button', 'aria-label' => _('Dismiss') }
- = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ = sprite_icon('close', css_class: 'gl-icon')
.gl-alert-body
- account_link_start = '<a class="deferred-link" href="%{url}">'.html_safe % { url: profile_account_path }
= _("Please ensure your account's %{account_link_start}recovery settings%{account_link_end} are up to date.").html_safe % { account_link_start: account_link_start, account_link_end: '</a>'.html_safe }
diff --git a/app/views/shared/_confirm_fork_modal.html.haml b/app/views/shared/_confirm_fork_modal.html.haml
index db50ea41387..f2a193e0bbc 100644
--- a/app/views/shared/_confirm_fork_modal.html.haml
+++ b/app/views/shared/_confirm_fork_modal.html.haml
@@ -1,4 +1,4 @@
-#modal-confirm-fork.modal.qa-confirm-fork-modal
+#modal-confirm-fork.modal{ data: { qa_selector: 'confirm_fork_modal' } }
.modal-dialog
.modal-content
.modal-header
@@ -9,4 +9,4 @@
%p= _("You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.") % { tag_start: '', tag_end: ''}
.modal-footer
= link_to _('Cancel'), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- = link_to _('Fork project'), fork_path, class: 'btn btn-success', method: :post
+ = link_to _('Fork project'), fork_path, class: 'btn btn-success', data: { qa_selector: 'fork_project_button' }, method: :post
diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml
index ecb462205b0..dc95bcdc756 100644
--- a/app/views/shared/_confirm_modal.html.haml
+++ b/app/views/shared/_confirm_modal.html.haml
@@ -17,5 +17,5 @@
.form-group
= text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input qa-confirm-input'
- .form-actions
+ .form-actions.gl-display-flex.gl-justify-content-end
= submit_tag _('Confirm'), class: "btn btn-danger js-confirm-danger-submit qa-confirm-button"
diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml
index 25c841d2344..ffc34ff34c3 100644
--- a/app/views/shared/_delete_label_modal.html.haml
+++ b/app/views/shared/_delete_label_modal.html.haml
@@ -8,7 +8,7 @@
.modal-body
%p
- = _('<strong>%{label_name}</strong> <span>will be permanently deleted from %{subject_name}. This cannot be undone.</span>').html_safe % { label_name: label.name, subject_name: label.subject_name }
+ = html_escape(_('%{label_name} %{span_open}will be permanently deleted from %{subject_name}. This cannot be undone.%{span_close}')) % { label_name: tag.strong(label.name), subject_name: label.subject_name, span_open: '<span>'.html_safe, span_close: '</span>'.html_safe }
.modal-footer
%a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }= _('Cancel')
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
deleted file mode 100644
index 076c87400e0..00000000000
--- a/app/views/shared/_field.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-- name = field[:name]
-- title = field[:title] || name.humanize
-- value = @service.send(name)
-- type = field[:type]
-- placeholder = field[:placeholder]
-- autocomplete = field[:autocomplete]
-- required = field[:required]
-- choices = field[:choices]
-- default_choice = field[:default_choice]
-- help = field[:help]
-
-.form-group.row
- - if type == "password" && value.present?
- = form.label name, _("Enter new %{field_title}") % { field_title: title.downcase }, class: "col-form-label col-sm-2"
- - else
- = form.label name, title, class: "col-form-label col-sm-2"
- .col-sm-10
- - if type == 'text'
- = form.text_field name, class: "form-control", autocomplete: autocomplete, placeholder: placeholder, required: required, data: { qa_selector: "#{name.downcase.gsub('\s', '')}_field" }
- - elsif type == 'textarea'
- = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder, required: required
- - elsif type == 'checkbox'
- = form.check_box name
- - elsif type == 'select'
- = form.select name, options_for_select(choices, value || default_choice), {}, { class: "form-control"}
- - elsif type == 'password'
- = form.password_field name, autocomplete: "new-password", placeholder: placeholder, class: "form-control", required: value.blank? && required, data: { qa_selector: "#{name.downcase.gsub('\s', '')}_field" }
- - if help
- %span.form-text.text-muted= help
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index b9952d6832f..a99c992af49 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -1,7 +1,7 @@
.file-content.code.js-syntax-highlight
.line-numbers
- if blob.data.present?
- - link_icon = icon('link')
+ - link_icon = sprite_icon('link', size: 12)
- link = blob_link if defined?(blob_link)
- blob.data.each_line.each_with_index do |_, index|
- offset = defined?(first_line_number) ? first_line_number : 1
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 09b9cd448bb..d497937833a 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -31,11 +31,11 @@
= _('Group path is already taken. Suggestions: ')
%span.gl-path-suggestions
%p.validation-success.gl-field-success.field-validation.hide= _('Group path is available.')
- %p.validation-pending.gl-field-error-ignore.field-validation.hide= _('Checking group path availability...')
+ %p.validation-pending.gl-field-error-ignore.field-validation.hide= _('Checking group URL availability...')
- if @group.persisted?
- .alert.alert-warning.prepend-top-10
- = _('Changing group path can have unintended side effects.')
+ .alert.alert-warning.gl-mt-3
+ = _('Changing group URL can have unintended side effects.')
= succeed '.' do
= link_to _('Learn more'), help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index b2ea45d6f1a..36d8aab6d53 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -26,8 +26,8 @@
.well-segment
%ul
%li
- = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
- %li= _('When using the <code>http://</code> or <code>https://</code> protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.').html_safe
+ = html_escape(_('The repository must be accessible over %{code_open}http://%{code_close}, %{code_open}https://%{code_close} or %{code_open}git://%{code_close}.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ %li= html_escape(_('When using the %{code_open}http://%{code_close} or %{code_open}https://%{code_close} protocols, please provide the exact URL to the repository. HTTP redirects will not be followed.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
= _('If your HTTP repository is not publicly accessible, add your credentials.')
%li
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index d704eae2090..3eb27f002ef 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -11,15 +11,15 @@
- if upvotes > 0
%li.issuable-upvotes.d-none.d-sm-block.has-tooltip{ title: _('Upvotes') }
- = sprite_icon('thumb-up', size: 16, css_class: "vertical-align-middle")
+ = sprite_icon('thumb-up', css_class: "vertical-align-middle")
= upvotes
- if downvotes > 0
%li.issuable-downvotes.d-none.d-sm-block.has-tooltip{ title: _('Downvotes') }
- = sprite_icon('thumb-down', size: 16, css_class: "vertical-align-middle")
+ = sprite_icon('thumb-down', css_class: "vertical-align-middle")
= downvotes
%li.issuable-comments.d-none.d-sm-block
- = link_to issuable_path, class: ['has-tooltip', ('no-comments' if note_count.zero?)], title: _('Comments') do
- = sprite_icon('comments', size: 16, css_class: 'gl-vertical-align-text-bottom')
+ = link_to issuable_path, class: ['has-tooltip', ('no-comments' if note_count == 0)], title: _('Comments') do
+ = sprite_icon('comments', css_class: 'gl-vertical-align-text-bottom')
= note_count
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index a21dcabb485..0f38d0e3b39 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,5 +1,5 @@
- if @issues.to_a.any?
- .card.card-small.card-without-border
+ .card.card-without-border
%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
= render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab"
diff --git a/app/views/shared/_md_preview.html.haml b/app/views/shared/_md_preview.html.haml
index c3818b9f7ae..c7c36d79fa0 100644
--- a/app/views/shared/_md_preview.html.haml
+++ b/app/views/shared/_md_preview.html.haml
@@ -2,7 +2,7 @@
- if defined?(@merge_request) && @merge_request.discussion_locked?
.issuable-note-warning
- = sprite_icon('lock', size: 16, css_class: 'icon')
+ = sprite_icon('lock', css_class: 'icon')
%span
= _('This merge request is locked.')
= _('Only project members can comment.')
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index 700ec4b606f..d280df8b370 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,5 +1,5 @@
- if @merge_requests.to_a.any?
- .card.card-small.card-without-border
+ .card.card-without-border
%ul.content-list.mr-list.issuable-list
= render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index ffa61c9d1a9..47fb38d979d 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,7 +1,7 @@
- if any_projects?(@projects)
.project-item-select-holder.btn-group
%a.btn.btn-success.new-project-item-link.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
- = icon('spinner spin')
+ = loading_icon(color: 'light')
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled]
- %button.btn.btn-success.new-project-item-select-button.qa-new-project-item-select-button
- = icon('caret-down')
+ %button.btn.btn-success.new-project-item-select-button.qa-new-project-item-select-button.gl-p-0
+ = sprite_icon('chevron-down')
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index 2b04e3e1c98..abf39fdc644 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,8 +1,8 @@
- if show_no_ssh_key_message?
%div{ class: 'no-ssh-key-message gl-alert gl-alert-warning', role: 'alert' }
- = sprite_icon('warning', size: 16, css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title')
+ = sprite_icon('warning', css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title')
%button{ class: 'gl-alert-dismiss hide-no-ssh-message', type: 'button', 'aria-label': _('Dismiss') }
- = sprite_icon('close', size: 16, css_class: 'gl-icon s16')
+ = sprite_icon('close', css_class: 'gl-icon s16')
.gl-alert-body
= s_("MissingSSHKeyWarningLink|You won't be able to pull or push project code via SSH until you add an SSH key to your profile").html_safe
.gl-alert-actions
diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml
index 30255e18f04..624cc99440c 100644
--- a/app/views/shared/_outdated_browser.html.haml
+++ b/app/views/shared/_outdated_browser.html.haml
@@ -1,6 +1,6 @@
- if outdated_browser?
.gl-alert.gl-alert-danger.outdated-browser{ :role => "alert" }
- = sprite_icon('error', size: 16, css_class: "gl-alert-icon gl-alert-icon-no-title gl-icon")
+ = sprite_icon('error', css_class: "gl-alert-icon gl-alert-icon-no-title gl-icon")
.gl-alert-body
- if browser.ie? && browser.version.to_i == 11
- feedback_link_url = 'https://gitlab.com/gitlab-org/gitlab/issues/197987'
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 7d93dca22f5..2425bcf61d9 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -11,28 +11,3 @@
- if @admin_integration
.js-vue-admin-integration-settings{ data: integration_form_data(@admin_integration) }
.js-vue-integration-settings{ data: integration_form_data(integration) }
-
- - if show_service_trigger_events?(integration)
- .form-group.row
- %label.col-form-label.col-sm-2= _('Trigger')
-
- .col-sm-10
- - integration.configurable_events.each do |event|
- .form-group
- .form-check
- = form.check_box service_event_field_name(event), class: 'form-check-input'
- = form.label service_event_field_name(event), class: 'form-check-label' do
- %strong
- = event.humanize
-
- - field = integration.event_field(event)
-
- - if field
- = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
-
- %p.text-muted
- = integration.class.event_description(event)
-
- - unless integration_form_refactor?
- - integration.global_fields.each do |field|
- = render 'shared/field', form: form, field: field
diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml
index 1431966c83d..9d1970093b8 100644
--- a/app/views/shared/_sidebar_toggle_button.html.haml
+++ b/app/views/shared/_sidebar_toggle_button.html.haml
@@ -4,5 +4,5 @@
%span.collapse-text= _("Collapse sidebar")
= button_tag class: 'close-nav-button', type: 'button' do
- = sprite_icon('close', size: 16)
+ = sprite_icon('close')
%span.collapse-text= _("Close sidebar")
diff --git a/app/views/shared/_zen.html.haml b/app/views/shared/_zen.html.haml
index 914409d0e65..66e0ecadb65 100644
--- a/app/views/shared/_zen.html.haml
+++ b/app/views/shared/_zen.html.haml
@@ -15,5 +15,5 @@
qa_selector: qa_selector }
- else
= text_area_tag attr, current_text, data: { qa_selector: qa_selector }, class: classes, placeholder: placeholder
- %a.zen-control.zen-control-leave.js-zen-leave.gl-text-gray-700{ href: "#" }
- = sprite_icon('compress', size: 16)
+ %a.zen-control.zen-control-leave.js-zen-leave.gl-text-gray-500{ href: "#" }
+ = sprite_icon('compress')
diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml
index 55231cb9429..ceac4d1820d 100644
--- a/app/views/shared/access_tokens/_table.html.haml
+++ b/app/views/shared/access_tokens/_table.html.haml
@@ -42,7 +42,7 @@
= _('In %{time_to_now}') % { time_to_now: distance_of_time_in_words_to_now(token.expires_at) }
- else
%span.token-never-expires-label= _('Never')
- %td= token.scopes.present? ? token.scopes.join(', ') : _('<no scopes selected>')
+ %td= token.scopes.present? ? token.scopes.join(', ') : html_escape_once(_('&lt;no scopes selected&gt;')).html_safe
%td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: 'btn btn-danger float-right qa-revoke-button', data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type } }
- else
.settings-message.text-center
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index b68c7cd4d52..7a4c495e177 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -32,7 +32,7 @@
- else
.boards-list.w-100.py-3.px-2.text-nowrap{ data: { qa_selector: "boards_list" } }
.boards-app-loading.w-100.text-center{ "v-if" => "loading" }
- = icon("spinner spin 2x")
+ = loading_icon(css_class: 'gl-mb-3')
%board{ "v-cloak" => "true",
"v-for" => "list in state.lists",
"ref" => "board",
diff --git a/app/views/shared/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml
index 117d56b30f5..d8ed3b13bf1 100644
--- a/app/views/shared/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml
@@ -1,9 +1,9 @@
.block.due_date
- .title
+ .title.gl-h-5.gl-display-flex.gl-align-items-center
= _("Due date")
- if can_admin_issue?
- = icon("spinner spin", class: "block-loading")
- = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
+ = loading_icon(css_class: 'gl-ml-2 block-loading')
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link gl-ml-auto"
.value
.value-content
%span.no-value{ "v-if" => "!issue.dueDate" }
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index 58ffa3942ef..61f3ebcdba4 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -1,9 +1,9 @@
.block.labels
- .title
+ .title.gl-h-5.gl-display-flex.gl-align-items-center
= _("Labels")
- if can_admin_issue?
- = icon("spinner spin", class: "block-loading")
- = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
+ = loading_icon(css_class: 'gl-ml-2 block-loading')
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link gl-ml-auto"
.value.issuable-show-labels.dont-hide
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
= _("None")
diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml
index b15d60002fc..510e05ce888 100644
--- a/app/views/shared/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml
@@ -1,9 +1,9 @@
.block.milestone
- .title
+ .title.gl-h-5.gl-display-flex.gl-align-items-center
= _("Milestone")
- if can_admin_issue?
- = icon("spinner spin", class: "block-loading")
- = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link float-right"
+ = loading_icon(css_class: 'gl-ml-2 block-loading')
+ = link_to _("Edit"), "#", class: "js-sidebar-dropdown-toggle edit-link gl-ml-auto"
.value
%span.no-value{ "v-if" => "!issue.milestone" }
= _("None")
diff --git a/app/views/shared/buttons/_project_feature_toggle.html.haml b/app/views/shared/buttons/_project_feature_toggle.html.haml
index 0f630786455..321fbee1b35 100644
--- a/app/views/shared/buttons/_project_feature_toggle.html.haml
+++ b/app/views/shared/buttons/_project_feature_toggle.html.haml
@@ -12,5 +12,5 @@
- if yield.present?
= yield
%span.toggle-icon
- = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
- = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
+ = sprite_icon('status_success_borderless', size: 18, css_class: 'gl-text-blue-500 toggle-status-checked')
+ = sprite_icon('status_failed_borderless', size: 18, css_class: 'gl-text-gray-400 toggle-status-unchecked')
diff --git a/app/views/shared/deploy_keys/_index.html.haml b/app/views/shared/deploy_keys/_index.html.haml
index 358075b9e44..f2f577383f8 100644
--- a/app/views/shared/deploy_keys/_index.html.haml
+++ b/app/views/shared/deploy_keys/_index.html.haml
@@ -1,5 +1,5 @@
- expanded = expanded_by_default?
-%section.qa-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_keys_settings' } }
+%section.qa-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_keys_settings_content' } }
.settings-header
%h4= _('Deploy Keys')
%button.btn.js-settings-toggle{ type: 'button' }
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 4e569050827..815967b0372 100644
--- a/app/views/shared/deploy_keys/_project_group_form.html.haml
+++ b/app/views/shared/deploy_keys/_project_group_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f|
+= form_for [@project.namespace, @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f|
= form_errors(@deploy_keys.new_key)
.form-group.row
= f.label :title, class: "label-bold"
@@ -20,5 +20,5 @@
%p.light.gl-mb-0
= _('Allow this key to push to repository as well? (Default only allows pull access.)')
- .form-group.row
- = f.submit "Add key", class: "btn-success btn"
+ .form-group.row.gl-display-flex.gl-justify-content-end
+ = f.submit _("Add key"), class: "btn-success btn"
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index 8d74e12e943..1eda439c9a5 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -45,5 +45,5 @@
= label_tag ("deploy_token_write_package_registry"), 'write_package_registry', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows write access to the package registry')
- .gl-mt-3
+ .gl-mt-3.gl-display-flex.gl-justify-content-end
= f.submit s_('DeployTokens|Create deploy token'), class: 'btn btn-success qa-create-deploy-token'
diff --git a/app/views/shared/deploy_tokens/_index.html.haml b/app/views/shared/deploy_tokens/_index.html.haml
index 8203b378297..540b9b0054f 100644
--- a/app/views/shared/deploy_tokens/_index.html.haml
+++ b/app/views/shared/deploy_tokens/_index.html.haml
@@ -1,6 +1,6 @@
- expanded = expand_deploy_tokens_section?(@new_deploy_token)
-%section.qa-deploy-tokens-settings.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_tokens_settings' } }
+%section.qa-deploy-tokens-settings.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_tokens_settings_content' } }
.settings-header
%h4= s_('DeployTokens|Deploy Tokens')
%button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
diff --git a/app/views/shared/deploy_tokens/_table.html.haml b/app/views/shared/deploy_tokens/_table.html.haml
index d4e20805a2a..ad73442807e 100644
--- a/app/views/shared/deploy_tokens/_table.html.haml
+++ b/app/views/shared/deploy_tokens/_table.html.haml
@@ -23,7 +23,7 @@
In #{distance_of_time_in_words_to_now(token.expires_at)}
- else
%span.token-never-expires-label= _('Never')
- %td= token.scopes.present? ? token.scopes.join(", ") : _('<no scopes selected>')
+ %td= token.scopes.present? ? token.scopes.join(", ") : html_escape_once(_('&lt;no scopes selected&gt;')).html_safe
%td= link_to s_('DeployTokens|Revoke'), "#", class: "btn btn-danger float-right", data: { toggle: "modal", target: "#revoke-modal-#{token.id}"}
= render 'shared/deploy_tokens/revoke_modal', token: token, group_or_project: group_or_project
- else
diff --git a/app/views/shared/empty_states/_deploy_keys.html.haml b/app/views/shared/empty_states/_deploy_keys.html.haml
new file mode 100644
index 00000000000..da34b866aa6
--- /dev/null
+++ b/app/views/shared/empty_states/_deploy_keys.html.haml
@@ -0,0 +1,9 @@
+.empty-state.gl-display-flex.gl-flex-direction-column.gl-flex-wrap.gl-text-center
+ .gl-flex-grow-0.gl-flex-shrink-0
+ .svg-250.svg-content
+ = image_tag 'illustrations/empty-state/empty-deploy-keys-lg.svg'
+ .gl-flex-grow-0.gl-flex-shrink-0
+ .text-content.gl-mx-auto.gl-my-0.gl-p-5
+ %h4.h4= _('Deploy keys allow read-only or read-write (if enabled) access to your repository')
+ %p= _('Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.')
+ = link_to _('New deploy key'), new_admin_deploy_key_path, class: 'btn btn-success btn-md gl-button'
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index eb5637acca0..3fd64291fb2 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -20,7 +20,7 @@
= _("To widen your search, change or remove filters above")
- if show_new_issue_link?(@project)
.text-center
- = link_to _("New issue"), new_project_issue_path(@project), class: "btn btn-success", id: "new_issue_body_link"
+ = link_to _("New issue"), new_project_issue_path(@project), class: "btn btn-success"
- elsif is_opened_state && opened_issues_count == 0 && closed_issues_count > 0
%h4.text-center
= _("There are no open issues")
@@ -28,7 +28,7 @@
= _("To keep this project going, create a new issue")
- if show_new_issue_link?(@project)
.text-center
- = link_to _("New issue"), new_project_issue_path(@project), class: "btn btn-success", id: "new_issue_body_link"
+ = link_to _("New issue"), new_project_issue_path(@project), class: "btn btn-success"
- elsif is_closed_state && opened_issues_count > 0 && closed_issues_count == 0
%h4.text-center
= _("There are no closed issues")
@@ -46,6 +46,16 @@
- if show_import_button
= render 'projects/issues/import_csv/button', type: :text
+ %hr
+ %p.gl-text-center.gl-mb-0
+ %strong
+ = s_('JiraService|Using Jira for issue tracking?')
+ %p.gl-text-center.gl-mb-0
+ - jira_docs_link_url = help_page_url('user/project/integrations/jira', anchor: 'view-jira-issues-premium')
+ - jira_docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: jira_docs_link_url }
+ = html_escape(s_('JiraService|%{jira_docs_link_start}Enable the Jira integration%{jira_docs_link_end} to view your Jira issues in GitLab.')) % { jira_docs_link_start: jira_docs_link_start.html_safe, jira_docs_link_end: '</a>'.html_safe }
+ %p.gl-text-center.gl-mb-0.gl-text-gray-500
+ = s_('JiraService|This feature requires a Premium plan.')
- else
%h4.text-center= _("There are no issues to show")
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index be5b1c6b6ce..837c3afc796 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -20,7 +20,7 @@
= _("To widen your search, change or remove filters above")
.text-center
- if can_create_merge_request
- = link_to _("New merge request"), project_new_merge_request_path(@project), class: "btn btn-success", title: _("New merge request"), id: "new_merge_request_body_link"
+ = link_to _("New merge request"), project_new_merge_request_path(@project), class: "btn btn-success", title: _("New merge request")
- elsif is_opened_state && opened_merged_count == 0 && closed_merged_count > 0
%h4.text-center
= _("There are no open merge requests")
@@ -28,7 +28,7 @@
= _("To keep this project going, create a new merge request")
.text-center
- if can_create_merge_request
- = link_to _("New merge request"), project_new_merge_request_path(@project), class: "btn btn-success", title: _("New merge request"), id: "new_merge_request_body_link"
+ = link_to _("New merge request"), project_new_merge_request_path(@project), class: "btn btn-success", title: _("New merge request")
- elsif is_closed_state && opened_merged_count > 0 && closed_merged_count == 0
%h4.text-center
= _("There are no closed merge requests")
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 5dac400bd5e..164773f9b60 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -16,14 +16,14 @@
.description
= markdown_field(group, :description)
- .stats.gl-text-gray-700.gl-flex-shrink-0
+ .stats.gl-text-gray-500.gl-flex-shrink-0
%span.gl-ml-5
- = icon('bookmark')
+ = sprite_icon('bookmark', css_class: 'gl-vertical-align-text-bottom')
= number_with_delimiter(group.projects.non_archived.count)
%span.gl-ml-5
- = icon('users')
+ = sprite_icon('users', css_class: 'gl-vertical-align-text-bottom')
= number_with_delimiter(group.users.count)
%span.gl-ml-5.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group) }
- = visibility_level_icon(group.visibility_level, fw: false)
+ = visibility_level_icon(group.visibility_level)
diff --git a/app/views/shared/integrations/_form.html.haml b/app/views/shared/integrations/_form.html.haml
index 4ec7f286c7a..5826cb280bd 100644
--- a/app/views/shared/integrations/_form.html.haml
+++ b/app/views/shared/integrations/_form.html.haml
@@ -1,14 +1,15 @@
- integration = local_assigns.fetch(:integration)
-%h3.page-title
- = integration.title
-
-%p= integration.description
-
-= form_for integration, as: :service, url: scoped_integration_path(integration), method: :put, html: { class: 'gl-show-field-errors fieldset-form integration-settings-form js-integration-settings-form', data: { 'can-test' => integration.can_test?, 'test-url' => scoped_test_integration_path(integration) } } do |form|
- = render 'shared/service_settings', form: form, integration: integration
-
- - if integration.editable?
- .footer-block.row-content-block
- = service_save_button
- = link_to _('Cancel'), scoped_integration_path(integration), class: 'btn btn-cancel'
+.row.gl-mt-3
+ .col-lg-4
+ %h3.page-title.gl-mt-0
+ = integration.title
+
+ .col-lg-8
+ = form_for integration, as: :service, url: scoped_integration_path(integration), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => integration.can_test?, 'test-url' => scoped_test_integration_path(integration) } } do |form|
+ = render 'shared/service_settings', form: form, integration: integration
+
+ - if integration.editable?
+ .footer-block.row-content-block
+ = service_save_button
+ = link_to _('Cancel'), scoped_integration_path(integration), class: 'btn btn-cancel'
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
index cec865ec8de..8e46db6dea2 100644
--- a/app/views/shared/issuable/_assignees.html.haml
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -6,5 +6,5 @@
- issuable.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(@project, assignee, name: false, title: "Assigned to :name")
-- if more_assignees_count.positive?
- %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', 'original-title' => "+#{more_assignees_count} more assignees", qa_selector: 'avatar_counter' } } +#{more_assignees_count}
+- if more_assignees_count > 0
+ %span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', 'original-title' => "+#{more_assignees_count} more assignees", qa_selector: 'avatar_counter_content' } } +#{more_assignees_count}
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index ec7ff127ed5..0c15d20bfe0 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -4,23 +4,24 @@
%aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? } }
.issuable-sidebar.hidden
- = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
+ = form_tag [:bulk_update, @project, type], method: :post, class: "bulk-update" do
.block.issuable-sidebar-header
.filter-item.inline.update-issues-btn.float-left
= button_tag _('Update all'), class: "btn update-selected-issues btn-info", disabled: true
= button_tag _('Cancel'), class: "btn btn-default js-bulk-update-menu-hide float-right"
- .block
- .title
- = _('Status')
- .filter-item
- = dropdown_tag(_("Select status"), options: { toggle_class: "js-issue-status", title: _("Change status"), dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: _("Status") } } ) do
- %ul
- %li
- %a{ href: "#", data: { id: "reopen" } }
- = _('Open')
- %li
- %a{ href: "#", data: { id: "close" } }
- = _('Closed')
+ - if params[:state] != 'merged'
+ .block
+ .title
+ = _('Status')
+ .filter-item
+ = dropdown_tag(_("Select status"), options: { toggle_class: "js-issue-status", title: _("Change status"), dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: _("Status") } } ) do
+ %ul
+ %li
+ %a{ href: "#", data: { id: "reopen" } }
+ = _('Open')
+ %li
+ %a{ href: "#", data: { id: "close" } }
+ = _('Closed')
.block
.title
= _('Assignee')
diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml
index 4fed95e2607..86c2e243718 100644
--- a/app/views/shared/issuable/_feed_buttons.html.haml
+++ b/app/views/shared/issuable/_feed_buttons.html.haml
@@ -1,4 +1,4 @@
-= link_to safe_params.merge(rss_url_options), class: 'btn has-tooltip js-rss-button', data: { container: 'body' }, title: _('Subscribe to RSS feed') do
- = sprite_icon('rss')
+= link_to safe_params.merge(rss_url_options), class: 'btn btn-svg has-tooltip', data: { container: 'body', testid: 'rss-feed-link' }, title: _('Subscribe to RSS feed') do
+ = sprite_icon('rss', css_class: 'qa-rss-icon')
= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do
= sprite_icon('calendar')
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index f54457b8b33..86cd2923fac 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -9,7 +9,7 @@
.alert.alert-danger
Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
Please check out
- = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer'
+ = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project, issuable]), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
@@ -63,11 +63,11 @@
.row-content-block{ class: (is_footer ? "footer-block" : "middle-block") }
.float-right
- if issuable.new_record?
- = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
+ = link_to 'Cancel', polymorphic_path([@project, issuable.class]), class: 'btn btn-cancel'
- else
- if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
- = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable], params: { destroy_confirm: true }), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped'
- = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
+ = link_to 'Delete', polymorphic_path([@project, issuable], params: { destroy_confirm: true }), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" }, method: :delete, class: 'btn btn-danger btn-grouped'
+ = link_to 'Cancel', polymorphic_path([@project, issuable]), class: 'btn btn-grouped btn-cancel'
%span.gl-mr-3
- if issuable.new_record?
@@ -76,11 +76,14 @@
= form.submit 'Save changes', class: 'btn btn-success'
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = issuable.project.present.contribution_guide_path)
- .inline.prepend-top-10
+ .inline.gl-mt-3
Please review the
%strong= link_to('contribution guidelines', guide_url)
for this project.
= render_if_exists 'shared/issuable/remove_approver'
+- if issuable.respond_to?(:issue_type)
+ = form.hidden_field :issue_type
+
= form.hidden_field :lock_version
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 0b5700e5413..3f3b9146e71 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -20,7 +20,8 @@
.issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
.filtered-search-box
- if type != :boards_modal && type != :boards
- = dropdown_tag(_('Recent searches'),
+ - text = tag.span(sprite_icon('history'), class: "d-md-none") + tag.span(_('Recent searches'), class: "d-none d-md-inline")
+ = dropdown_tag(text,
options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
toggle_class: "btn filtered-search-history-dropdown-toggle-button",
dropdown_class: "filtered-search-history-dropdown",
@@ -173,7 +174,7 @@
= render 'shared/issuable/board_create_list_dropdown', board: board
- if @project
#js-add-issues-btn.gl-ml-3{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
- - if Feature.enabled?(:boards_with_swimlanes, @group)
+ - if current_user && Feature.enabled?(:boards_with_swimlanes, @group)
#js-board-epics-swimlanes-toggle
#js-toggle-focus-btn
- elsif is_not_boards_modal_or_productivity_analytics && show_sorting_dropdown
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 00113b2c2c0..6f31d7290b7 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -32,7 +32,7 @@
- milestone = issuable_sidebar[:milestone] || {}
.block.milestone{ data: { qa_selector: 'milestone_block' } }
.sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
- = icon('clock-o', 'aria-hidden': 'true')
+ = sprite_icon('clock')
%span.milestone-title.collapse-truncated-title
- if milestone.present?
= milestone[:title]
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index 1823c5279e5..30a1f0febc3 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -9,7 +9,7 @@
.form-group.row.d-flex.gl-pl-3-deprecated-no-really-do-not-use-me.gl-pr-3-deprecated-no-really-do-not-use-me.branch-selector
.align-self-center
%span
- = _('From <code>%{source_title}</code> into').html_safe % { source_title: source_title }
+ = html_escape(_('From %{code_open}%{source_title}%{code_close} into')) % { source_title: source_title, code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- if issuable.new_record?
%code#js-target-branch-title= target_title
@@ -17,7 +17,9 @@
= link_to _('Change branches'), mr_change_branches_path(issuable)
- elsif issuable.for_fork?
%code= issuable.target_project_path + ":"
- - unless issuable.new_record?
+ - if issuable.merged?
+ %code= target_title
+ - unless issuable.new_record? || issuable.merged?
%span.dropdown.gl-ml-2.d-inline-block
= form.hidden_field(:target_branch,
{ class: 'target_branch js-target-branch-select ref-name mw-xl',
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index 6f1023474a1..5c5c8c816d3 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -27,5 +27,5 @@
Squash commits when merge request is accepted.
= link_to icon('question-circle'), help_page_path('user/project/merge_requests/squash_and_merge'), target: '_blank'
- if project.squash_always?
- .gl-text-gray-600
+ .gl-text-gray-400
= _('Required in this project.')
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index 355a6627b8f..98c9f73fa3a 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -3,6 +3,13 @@
- form = local_assigns.fetch(:form)
- no_issuable_templates = issuable_templates(issuable).empty?
- div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8'
+- toggle_wip_link_start = '<a href="" class="js-toggle-wip">'
+- toggle_wip_link_end = '</a>'
+- draft_snippet = '<code>Draft:</code>'.html_safe
+- wip_snippet = '<code>WIP:</code>'.html_safe
+- draft_or_wip_snippet = '<code>Draft/WIP</code>'.html_safe
+- add_wip_text = (_('%{link_start}Start the title with %{draft_snippet} or %{wip_snippet}%{link_end} to prevent a merge request that is a work in progress from being merged before it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: draft_snippet, wip_snippet: wip_snippet } ).html_safe
+- remove_wip_text = (_('%{link_start}Remove the %{draft_or_wip_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it\'s ready.' ) % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_or_wip_snippet: draft_or_wip_snippet } ).html_safe
%div{ class: div_class }
= form.text_field :title, required: true, maxlength: 255, autofocus: true,
@@ -11,23 +18,12 @@
- if issuable.respond_to?(:work_in_progress?)
.form-text.text-muted
.js-wip-explanation
- %a.js-toggle-wip{ href: '' }
- Remove the
- %code WIP:
- prefix from the title
- to allow this
- %strong Work In Progress
- merge request to be merged when it's ready.
+ = remove_wip_text
.js-no-wip-explanation
- if has_wip_commits
- It looks like you have some WIP commits in this branch.
+ = _('It looks like you have some draft commits in this branch.')
%br
- %a.js-toggle-wip{ href: '' }
- Start the title with
- %code WIP:
- to prevent a
- %strong Work In Progress
- merge request from being merged before it's ready.
+ = add_wip_text
- if no_issuable_templates && can?(current_user, :push_code, issuable.project)
= render 'shared/issuable/form/default_templates'
diff --git a/app/views/shared/members/_filter_2fa_dropdown.html.haml b/app/views/shared/members/_filter_2fa_dropdown.html.haml
index 44ea844028e..a2bc5e9ecdf 100644
--- a/app/views/shared/members/_filter_2fa_dropdown.html.haml
+++ b/app/views/shared/members/_filter_2fa_dropdown.html.haml
@@ -1,6 +1,6 @@
- filter = params[:two_factor] || 'everyone'
- filter_options = { 'everyone' => _('Everyone'), 'enabled' => _('Enabled'), 'disabled' => _('Disabled') }
-.dropdown.inline.member-filter-2fa-dropdown.pr-md-2
+.dropdown.inline.member-filter-2fa-dropdown
= dropdown_toggle(filter_options[filter], { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 1d7d18d2ab6..f59e0f92c60 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -46,4 +46,4 @@
class: 'btn btn-remove m-0 ml-sm-2 align-self-center' do
%span.d-block.d-sm-none
= _("Delete")
- = icon('trash', class: 'd-none d-sm-block')
+ = sprite_icon('remove', css_class: 'd-none d-sm-block')
diff --git a/app/views/shared/members/_invite_group.html.haml b/app/views/shared/members/_invite_group.html.haml
index 27c930bcbb5..a2fb33aa757 100644
--- a/app/views/shared/members/_invite_group.html.haml
+++ b/app/views/shared/members/_invite_group.html.haml
@@ -14,7 +14,7 @@
.select-wrapper
= select_tag group_access_field, options_for_select(access_levels, default_access_level), data: { qa_selector: 'group_access_field' }, class: "form-control select-control"
= icon('chevron-down')
- .form-text.text-muted.append-bottom-10
+ .form-text.text-muted.gl-mb-3
- permissions_docs_path = help_page_path('user/permissions')
- link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path }
= _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/shared/members/_invite_member.html.haml b/app/views/shared/members/_invite_member.html.haml
index d3a1c85e285..284d7fdb6da 100644
--- a/app/views/shared/members/_invite_member.html.haml
+++ b/app/views/shared/members/_invite_member.html.haml
@@ -14,7 +14,7 @@
.select-wrapper
= select_tag :access_level, options_for_select(access_levels, default_access_level), class: "form-control project-access-select select-control"
= icon('chevron-down')
- .form-text.text-muted.append-bottom-10
+ .form-text.text-muted.gl-mb-3
- permissions_docs_path = help_page_path('user/permissions')
- link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path }
= _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 79dc3043e8d..fa71f4dc9b9 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -33,7 +33,7 @@
- if source.instance_of?(Group) && source != @group
&middot;
- = link_to source.full_name, source, class: "member-group-link"
+ = link_to source.full_name, source, class: "gl-display-inline-block inline-link"
.cgray
- if member.request?
@@ -62,7 +62,7 @@
- if show_controls && member.source == current_resource
- if member.can_resend_invite?
- = link_to sprite_icon('paper-airplane', size: 16), polymorphic_path([:resend_invite, member]),
+ = link_to sprite_icon('paper-airplane'), polymorphic_path([:resend_invite, member]),
method: :post,
class: 'btn btn-default align-self-center mr-sm-2',
title: _('Resend invite')
@@ -113,18 +113,17 @@
- if member.can_remove?
- if current_user == user
- = link_to icon('sign-out', text: _('Leave')), polymorphic_path([:leave, member.source, :members]),
- method: :delete,
- data: { confirm: leave_confirmation_message(member.source) },
- class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}"
+ = link_to polymorphic_path([:leave, member.source, :members]), method: :delete, data: { confirm: leave_confirmation_message(member.source) }, class: "btn gl-button btn-svg btn-danger align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}" do
+ = sprite_icon('leave', css_class: 'gl-icon')
+ = _('Leave')
- else
%button{ data: { member_path: member_path(member.member), message: remove_member_message(member), is_access_request: member.request?.to_s, qa_selector: 'delete_member_button' },
- class: "js-remove-member-button btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}",
+ class: "js-remove-member-button btn gl-button btn-danger align-self-center m-0 #{'ml-sm-2 btn-icon' unless force_mobile_view}",
title: remove_member_title(member) }
%span{ class: ('d-block d-sm-none' unless force_mobile_view) }
= _("Delete")
- unless force_mobile_view
- = icon('trash', class: 'd-none d-sm-block')
+ = sprite_icon('remove', css_class: 'd-none d-sm-block gl-icon')
= render_if_exists 'shared/members/ee/override_member_buttons', group: @group, member: member, user: user, action: :edit, can_override: member.can_override?
- else
%span.member-access-text.user-access-role= member.human_access
diff --git a/app/views/shared/members/_search_field.html.haml b/app/views/shared/members/_search_field.html.haml
new file mode 100644
index 00000000000..e70cb063324
--- /dev/null
+++ b/app/views/shared/members/_search_field.html.haml
@@ -0,0 +1,6 @@
+- name = local_assigns.fetch(:name, :search)
+
+.search-control-wrap.gl-relative
+ = search_field_tag name, params[name], { placeholder: _('Search'), class: 'form-control', spellcheck: false }
+ %button.user-search-btn.border-left.gl-display-flex.gl-align-items-center.gl-justify-content-center{ type: 'submit', 'aria': { label: _('Submit search') } }
+ = sprite_icon('search')
diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml
index 50a55565c3c..606d3bcdfa8 100644
--- a/app/views/shared/members/_sort_dropdown.html.haml
+++ b/app/views/shared/members/_sort_dropdown.html.haml
@@ -1,4 +1,3 @@
-= label_tag :sort_by, 'Sort by', class: 'col-form-label label-bold px-2'
.dropdown.inline.qa-user-sort-dropdown
= dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
diff --git a/app/views/shared/milestones/_deprecation_message.html.haml b/app/views/shared/milestones/_deprecation_message.html.haml
deleted file mode 100644
index 27cd6d75232..00000000000
--- a/app/views/shared/milestones/_deprecation_message.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-.banner-callout.compact.milestone-deprecation-message.js-milestone-deprecation-message.prepend-top-20
- .banner-graphic= image_tag 'illustrations/milestone_removing-page.svg'
- .banner-body.gl-ml-3.gl-mr-3
- %h5.banner-title.gl-mt-0= _('This page will be removed in a future release.')
- %p.milestone-banner-text= _('Use group milestones to manage issues from multiple projects in the same milestone.')
- = button_tag _('Promote these project milestones into a group milestone.'), class: 'btn btn-link js-popover-link text-align-left milestone-banner-link'
- .milestone-banner-buttons.prepend-top-20= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-default', target: '_blank'
-
- %template.js-milestone-deprecation-message-template
- .milestone-popover-body
- %ol.milestone-popover-instructions-list.gl-mb-0
- %li= _('Click any <strong>project name</strong> in the project list below to navigate to the project milestone.').html_safe
- %li= _('Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.').html_safe
- %hr.popover-hr
- .milestone-popover-footer= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-link prepend-left-0', target: '_blank'
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index ae5bf9572bd..4ef8a9dd842 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -15,7 +15,7 @@
.text-tertiary.gl-mb-2
= milestone_date_range(milestone)
- recent_releases, total_count, more_count = recent_releases_with_counts(milestone)
- - unless total_count.zero?
+ - unless total_count == 0
.text-tertiary.gl-mb-2.milestone-release-links
= sprite_icon("rocket", size: 12)
= n_('Release', 'Releases', total_count)
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 7fd657ec2dd..bdacdb23141 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -140,11 +140,11 @@
.block.releases
.sidebar-collapsed-icon.has-tooltip{ title: milestone_releases_tooltip_text(milestone), data: { container: 'body', placement: 'left', boundary: 'viewport' } }
%strong
- = sprite_icon("rocket", size: 16)
+ = sprite_icon("rocket")
%span= total_count
.title.hide-collapsed= n_('Release', 'Releases', total_count)
.hide-collapsed
- - if total_count.zero?
+ - if total_count == 0
.no-value= s_('MilestoneSidebar|None')
- else
.font-weight-bold
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index abd5d8cd9db..51c1ee0c4d1 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -12,7 +12,7 @@
%span.uploading-container
%span.uploading-progress-container.hide
- = sprite_icon('media', size: 16, css_class: 'gl-icon gl-vertical-align-text-bottom')
+ = sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
%span.attaching-file-message
-# Populated by app/assets/javascripts/dropzone_input.js
%span.uploading-progress 0%
@@ -20,7 +20,7 @@
%span.uploading-error-container.hide
%span.uploading-error-icon
- = sprite_icon('media', size: 16, css_class: 'gl-icon gl-vertical-align-text-bottom')
+ = sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
%span.uploading-error-message
-# Populated by app/assets/javascripts/dropzone_input.js
%button.retry-uploading-link{ type: 'button' }= _("Try again")
@@ -28,7 +28,7 @@
%button.attach-new-file.markdown-selector{ type: 'button' }= _("attach a new file")
%button.btn.markdown-selector.button-attach-file.btn-link{ type: 'button', tabindex: '-1' }
- = sprite_icon('media', size: 16)
+ = sprite_icon('media')
%span.text-attach-file<>
= _("Attach a file")
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 95450a5df3c..da665f17975 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -63,7 +63,8 @@
- if note.system
.system-note-commit-list-toggler.hide
= _("Toggle commit list")
- %i.fa.fa-angle-down
+ = sprite_icon('chevron-down', css_class: 'js-chevron-down gl-ml-1 gl-vertical-align-text-bottom')
+ = sprite_icon('chevron-up', css_class: 'js-chevron-up gl-ml-1 gl-vertical-align-text-bottom gl-display-none')
- if note.attachment.url
.note-attachment
- if note.attachment.image?
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index fa103ad447a..2e98b06ec4a 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -25,8 +25,8 @@
- elsif discussion_locked
.disabled-comment.text-center.gl-mt-3
%span.issuable-note-warning
- = sprite_icon('lock', size: 16, css_class: 'icon')
+ = sprite_icon('lock', css_class: 'icon')
%span
- = _("This %{issuable} is locked. Only <strong>project members</strong> can comment.").html_safe % { issuable: issuable.class.to_s.titleize.downcase }
+ = html_escape(_("This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment.")) % { issuable: issuable.class.to_s.titleize.downcase, strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
-# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 2b3e986a841..f2c7ab648c0 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -17,16 +17,16 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
- = icon("bell", class: "js-notification-loading")
+ %button.dropdown-new.btn.btn-defaul.btn-icon.gl-button.has-tooltip.notifications-btn.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } }
+ = sprite_icon("notifications", css_class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle.d-flex{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
- %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
+ %button.dropdown-new.btn.btn-default.btn-icon.gl-button.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
.float-left
- = icon("bell", class: "js-notification-loading")
+ = sprite_icon("notifications", css_class: "js-notification-loading")
= notification_title(notification_setting.level)
.float-right
= icon("caret-down")
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index 9bd08c2296f..51b7da7dee8 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -23,7 +23,6 @@
#{ paragraph.html_safe }
.col-lg-8
- notification_setting.email_events.each_with_index do |event, index|
- - next if notification_event_disabled?(event)
- field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
.form-group
.form-check{ class: ("gl-mt-0" if index == 0) }
diff --git a/app/views/shared/packages/_no_packages.html.haml b/app/views/shared/packages/_no_packages.html.haml
new file mode 100644
index 00000000000..ae5c2cfd378
--- /dev/null
+++ b/app/views/shared/packages/_no_packages.html.haml
@@ -0,0 +1,7 @@
+.svg-content= image_tag 'illustrations/no-packages.svg'
+.text-content
+ %h4.text-center= _('There are no packages yet')
+ %p
+ - no_packages_url = help_page_path('administration/packages/index')
+ - no_packages_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: no_packages_url }
+ = _('Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab.').html_safe % { no_packages_link_start: no_packages_link_start, no_packages_link_end: '</a>'.html_safe }
diff --git a/app/views/shared/projects/_archived.html.haml b/app/views/shared/projects/_archived.html.haml
index fad93d14390..f24fe3a8b89 100644
--- a/app/views/shared/projects/_archived.html.haml
+++ b/app/views/shared/projects/_archived.html.haml
@@ -1,3 +1,3 @@
- if project.archived
- %span.d-flex.badge.badge-warning
+ %span.d-flex.badge-pill.gl-badge.badge-warning.gl-ml-3
= _('archived')
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 626e94e0202..115d0c9a7c5 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -26,7 +26,7 @@
= image_tag avatar_icon_for_user(project.creator, 48), class: "avatar s48", alt:''
- else
= project_icon(project, alt: '', class: 'avatar project-avatar s48', width: 48, height: 48)
- .project-details.d-sm-flex.flex-sm-fill.align-items-center{ data: { qa_selector: 'project', qa_project_name: project.name } }
+ .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
%h2.d-flex.gl-mt-3
@@ -40,7 +40,7 @@
= project.name
%span.metadata-info.visibility-icon.gl-mr-3.gl-mt-3.text-secondary.has-tooltip{ data: { container: 'body', placement: 'top' }, title: visibility_icon_description(project) }
- = visibility_level_icon(project.visibility_level, fw: true)
+ = visibility_level_icon(project.visibility_level)
- if explore_projects_tab? && project_license_name(project)
%span.metadata-info.d-inline-flex.align-items-center.gl-mr-3.gl-mt-3
@@ -51,7 +51,7 @@
-# haml-lint:disable UnnecessaryStringOutput
= ' ' # prevent haml from eating the space between elements
.metadata-info.gl-mt-3
- %span.user-access-role.d-block= Gitlab::Access.human_access(access)
+ %span.user-access-role.d-block{ data: { qa_selector: 'user_role_content' } }= Gitlab::Access.human_access(access)
- if !explore_projects_tab?
.metadata-info.gl-mt-3
@@ -64,6 +64,8 @@
.description.d-none.d-sm-block.gl-mr-3
= markdown_field(project, :description)
+ = render_if_exists 'shared/projects/removed', project: project
+
.controls.d-flex.flex-sm-column.align-items-center.align-items-sm-end.flex-wrap.flex-shrink-0.text-secondary{ class: css_controls_class.join(" ") }
.icon-container.d-flex.align-items-center
- if show_pipeline_status_icon
diff --git a/app/views/shared/projects/_search_bar.html.haml b/app/views/shared/projects/_search_bar.html.haml
index c1f2eaba284..a745da32110 100644
--- a/app/views/shared/projects/_search_bar.html.haml
+++ b/app/views/shared/projects/_search_bar.html.haml
@@ -14,7 +14,7 @@
.filtered-search-box-input-container.pl-2
= render 'shared/projects/search_form', admin_view: false, search_form_placeholder: _("Search projects...")
%button.btn.btn-secondary{ type: 'submit', form: 'project-filter-form' }
- = sprite_icon('search', size: 16, css_class: 'search-icon ')
+ = sprite_icon('search', css_class: 'search-icon ')
.filtered-search-dropdown.flex-row.align-items-center.mb-2.m-sm-0#filtered-search-visibility-dropdown{ class: flex_grow_and_shrink_xs }
.filtered-search-dropdown-label.p-0.pl-sm-3.font-weight-bold
%span
@@ -25,4 +25,3 @@
%span
= _("Sort by")
= render 'shared/projects/sort_dropdown'
-
diff --git a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml b/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml
new file mode 100644
index 00000000000..eafc402f210
--- /dev/null
+++ b/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml
@@ -0,0 +1,35 @@
+- merge_access_levels = protected_branch.merge_access_levels.for_role
+- push_access_levels = protected_branch.push_access_levels.for_role
+
+- user_merge_access_levels = protected_branch.merge_access_levels.for_user
+- user_push_access_levels = protected_branch.push_access_levels.for_user
+
+- group_merge_access_levels = protected_branch.merge_access_levels.for_group
+- group_push_access_levels = protected_branch.push_access_levels.for_group
+
+%td.merge_access_levels-container
+ = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level
+ = dropdown_tag( (merge_access_levels.first&.humanize || 'Select') ,
+ options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
+ data: { field_name: "allowed_to_merge_#{protected_branch.id}", preselected_items: access_levels_data(merge_access_levels) }})
+ - if user_merge_access_levels.any?
+ %p.small
+ = _('The following %{user} can also merge into this branch: %{branch}') % { user: 'user'.pluralize(user_merge_access_levels.size), branch: user_merge_access_levels.map(&:humanize).to_sentence }
+
+ - if group_merge_access_levels.any?
+ %p.small
+ = _('Members of %{group} can also merge into this branch: %{branch}') % { group: (group_merge_access_levels.size > 1 ? 'these groups' : 'this group'), branch: group_merge_access_levels.map(&:humanize).to_sentence }
+
+%td.push_access_levels-container
+ = hidden_field_tag "allowed_to_push_#{protected_branch.id}", push_access_levels.first&.access_level
+ = dropdown_tag( (push_access_levels.first&.humanize || 'Select') ,
+ options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
+ data: { field_name: "allowed_to_push_#{protected_branch.id}", preselected_items: access_levels_data(push_access_levels) }})
+ - if user_push_access_levels.any?
+ %p.small
+ = _('The following %{user} can also push to this branch: %{branch}') % { user: 'user'.pluralize(user_push_access_levels.size), branch: user_push_access_levels.map(&:humanize).to_sentence }
+
+ - if group_push_access_levels.any?
+ %p.small
+ = _('Members of %{group} can also push to this branch: %{branch}') % { group: (group_push_access_levels.size > 1 ? 'these groups' : 'this group'), branch: group_push_access_levels.map(&:humanize).to_sentence }
+
diff --git a/app/views/shared/promotions/_promote_servicedesk.html.haml b/app/views/shared/promotions/_promote_servicedesk.html.haml
index f7f65c34c75..fbac5ef0bbd 100644
--- a/app/views/shared/promotions/_promote_servicedesk.html.haml
+++ b/app/views/shared/promotions/_promote_servicedesk.html.haml
@@ -5,9 +5,9 @@
.svg-container
= custom_icon('icon_service_desk')
.user-callout-copy
- -# haml-lint:disable NoPlainNodes
%h4
- Improve customer support with GitLab Service Desk.
+ = _("Improve customer support with GitLab Service Desk.")
%p
- GitLab Service Desk is a simple way to allow people to create issues in your GitLab instance without needing their own user account. It provides a unique email address for end users to create issues in a project, and replies can be sent either through the GitLab interface or by email. End users will only see the thread through email.
- = link_to 'Read more', help_page_path('user/project/service_desk.md'), target: '_blank'
+ = _("GitLab Service Desk is a simple way to allow people to create issues in your GitLab instance without needing their own user account. It provides a unique email address for end users to create issues in a project, and replies can be sent either through the GitLab interface or by email. End users will only see the thread through email.")
+ = link_to _('Read more'), help_page_path('user/project/service_desk.md'), target: '_blank'
+
diff --git a/app/views/shared/snippets/_embed.html.haml b/app/views/shared/snippets/_embed.html.haml
index f1abd3a2ce4..f698e1a301b 100644
--- a/app/views/shared/snippets/_embed.html.haml
+++ b/app/views/shared/snippets/_embed.html.haml
@@ -5,17 +5,17 @@
%strong.file-title-name
%a.gitlab-embedded-snippets-title{ href: url_for(only_path: false, overwrite_params: nil) }
- = @blob.name
+ = blob.name
%small
- = number_to_human_size(@blob.raw_size)
+ = number_to_human_size(blob.size)
%a.gitlab-logo-wrapper{ href: url_for(only_path: false, overwrite_params: nil), title: 'view on gitlab' }
%img.gitlab-logo{ src: image_url('ext_snippet_icons/logo.svg'), alt: "GitLab logo" }
.file-actions.d-none.d-sm-block
.btn-group{ role: "group" }<
- = embedded_raw_snippet_button
+ = embedded_raw_snippet_button(@snippet, blob)
- = embedded_snippet_download_button
+ = embedded_snippet_download_button(@snippet, blob)
%article.file-holder.snippet-file-content
- = render 'projects/blob/viewer', viewer: @blob.simple_viewer, load_async: false, external_embed: true
+ = render 'projects/blob/viewer', viewer: blob.simple_viewer, load_async: false, external_embed: true
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 7f307f33b51..81277b50d13 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -29,7 +29,8 @@
.js-file-title.file-title-flex-parent
= f.text_field :file_name, placeholder: s_("Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby"), class: 'form-control js-snippet-file-name', data: { qa_selector: 'file_name_field' }
.file-content.code
- %pre#editor{ data: { 'editor-loading': true } }= @snippet.content
+ #editor{ data: { 'editor-loading': true } }<
+ %pre.editor-loading-content= @snippet.content
= f.hidden_field :content, class: 'snippet-file-content'
.form-group
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 36b6bfd061f..d6019e45b25 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -3,7 +3,7 @@
.snippet-box.has-tooltip.inline.gl-mr-2{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
%span.sr-only
= visibility_level_label(@snippet.visibility_level)
- = visibility_level_icon(@snippet.visibility_level, fw: false)
+ = visibility_level_icon(@snippet.visibility_level)
%span.creator
Authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index b2c9a74b177..25e31fd519b 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -10,13 +10,13 @@
%ul.controls
%li
- = link_to gitlab_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count.zero?) do
- = sprite_icon('comments', size: 16, css_class: 'gl-vertical-align-text-bottom')
+ = link_to gitlab_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if notes_count == 0) do
+ = sprite_icon('comments', css_class: 'gl-vertical-align-text-bottom')
= notes_count
%li
%span.sr-only
= visibility_level_label(snippet.visibility_level)
- = visibility_level_icon(snippet.visibility_level, fw: false)
+ = visibility_level_icon(snippet.visibility_level)
.snippet-info
#{snippet.to_reference} &middot;
diff --git a/app/views/shared/snippets/show.js.haml b/app/views/shared/snippets/show.js.haml
index d552c1a723b..23cebc97f63 100644
--- a/app/views/shared/snippets/show.js.haml
+++ b/app/views/shared/snippets/show.js.haml
@@ -1,2 +1,2 @@
document.write('#{escape_javascript(stylesheet_link_tag("#{stylesheet_url 'snippets'}"))}');
-document.write('#{escape_javascript(render('shared/snippets/embed'))}');
+document.write('#{escape_javascript(render(partial: 'shared/snippets/embed', collection: @blobs, as: :blob))}');
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index ce85cbd7f07..0f6188fa334 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -72,6 +72,12 @@
%strong Wiki Page events
%p.text-muted.ml-1
This URL will be triggered when a wiki page is created/updated
+ %li
+ = form.check_box :deployment_events, class: 'form-check-input'
+ = form.label :deployment_events, class: 'list-label form-check-label ml-1' do
+ %strong= s_('Webhooks|Deployment events')
+ %p.text-muted.ml-1
+ = s_('Webhooks|This URL will be triggered when a deployment is finished/failed/canceled')
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'label-bold checkbox'
.form-check
diff --git a/app/views/shared/wikis/_form.html.haml b/app/views/shared/wikis/_form.html.haml
index 92b9207aaa4..4d64521f9b0 100644
--- a/app/views/shared/wikis/_form.html.haml
+++ b/app/views/shared/wikis/_form.html.haml
@@ -59,7 +59,7 @@
- link_example = '[[page-slug]]'
- else
- link_example = '[Link Title](page-slug)'
- = (s_('WikiMarkdownTip|To link to a (new) page, simply type <code class="js-markup-link-example">%{link_example}</code>') % { link_example: link_example }).html_safe
+ = html_escape(s_('WikiMarkdownTip|To link to a (new) page, simply type %{link_example}')) % { link_example: tag.code(link_example, class: 'js-markup-link-example') }
= succeed '.' do
- markdown_link = link_to s_("WikiMarkdownDocs|documentation"), help_page_path('user/markdown', anchor: 'wiki-specific-markdown')
= (s_("WikiMarkdownDocs|More examples are in the %{docs_link}") % { docs_link: markdown_link }).html_safe
diff --git a/app/views/shared/wikis/_sidebar.html.haml b/app/views/shared/wikis/_sidebar.html.haml
index cddf19fbc8e..54f285671a1 100644
--- a/app/views/shared/wikis/_sidebar.html.haml
+++ b/app/views/shared/wikis/_sidebar.html.haml
@@ -2,11 +2,11 @@
.sidebar-container
.block.wiki-sidebar-header.gl-mb-3.w-100
%a.gutter-toggle.float-right.d-block.d-sm-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" }
- = sprite_icon('chevron-double-lg-right', size: 16, css_class: 'gl-icon')
+ = sprite_icon('chevron-double-lg-right', css_class: 'gl-icon')
- git_access_url = wiki_path(@wiki, action: :git_access)
= link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do
- = sprite_icon('download', size: 16, css_class: 'gl-mr-2')
+ = sprite_icon('download', css_class: 'gl-mr-2')
%span= _("Clone repository")
.blocks-container
diff --git a/app/views/shared/wikis/_wiki_directory.html.haml b/app/views/shared/wikis/_wiki_directory.html.haml
index 0e5f32ed859..21e829d86a6 100644
--- a/app/views/shared/wikis/_wiki_directory.html.haml
+++ b/app/views/shared/wikis/_wiki_directory.html.haml
@@ -1,4 +1,4 @@
-%li
+%li{ data: { qa_selector: 'wiki_directory_content' } }
= wiki_directory.slug
%ul
= render wiki_directory.pages, context: context
diff --git a/app/views/snippets/verify.html.haml b/app/views/snippets/verify.html.haml
index cb623ccab57..3c4f08e1df7 100644
--- a/app/views/snippets/verify.html.haml
+++ b/app/views/snippets/verify.html.haml
@@ -1,4 +1,2 @@
-- form = [@snippet.becomes(Snippet)]
-
-= render 'layouts/recaptcha_verification', spammable: @snippet, form: form
+= render 'layouts/recaptcha_verification', spammable: @snippet
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index 6f3f4c4981c..a83b55379da 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -6,13 +6,13 @@
%script#js-register-u2f-setup{ type: "text/template" }
- if current_user.two_factor_otp_enabled?
- .row.append-bottom-10
+ .row.gl-mb-3
.col-md-4
%button#js-setup-u2f-device.btn.btn-info.btn-block= _("Set up new U2F device")
.col-md-8
%p= _("Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.")
- else
- .row.append-bottom-10
+ .row.gl-mb-3
.col-md-4
%button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true }= _("Set up new U2F device")
.col-md-8
@@ -28,11 +28,11 @@
%a.btn.btn-warning#js-token-2fa-try-again= _("Try again?")
%script#js-register-u2f-registered{ type: "text/template" }
- .row.append-bottom-10
+ .row.gl-mb-3
.col-md-12
%p= _("Your device was successfully set up! Give it a name and register it with the GitLab server.")
= form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
- .row.append-bottom-10
+ .row.gl-mb-3
.col-md-3
= text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: _("Pick a name")
.col-md-3
diff --git a/app/views/users/_deletion_guidance.html.haml b/app/views/users/_deletion_guidance.html.haml
index 0024801dbf6..507fe126acb 100644
--- a/app/views/users/_deletion_guidance.html.haml
+++ b/app/views/users/_deletion_guidance.html.haml
@@ -6,6 +6,6 @@
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path("user/profile/account/delete_account", anchor: "associated-records") }
= _('Certain user content will be moved to a system-wide "Ghost User" in order to maintain content for posterity. For further information, please refer to the %{link_start}user account deletion documentation.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- personal_projects_count = user.personal_projects.count
- - unless personal_projects_count.zero?
+ - unless personal_projects_count == 0
%li
= n_('%d personal project will be removed and cannot be restored.', '%d personal projects will be removed and cannot be restored.', personal_projects_count) % personal_projects_count
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index a5197a9950b..2f44a57c388 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -1,12 +1,12 @@
%h4.prepend-top-20
- = _("Contributions for <strong>%{calendar_date}</strong>").html_safe % { calendar_date: @calendar_date.to_s(:medium) }
+ = html_escape(_("Contributions for %{calendar_date}")) % { calendar_date: tag.strong(@calendar_date.to_s(:medium)) }
- if @events.any?
%ul.bordered-list
- @events.sort_by(&:created_at).each do |event|
%li
%span.light
- %i.fa.fa-clock-o
+ = sprite_icon('clock', css_class: 'gl-vertical-align-text-bottom')
= event.created_at.to_time.in_time_zone.strftime('%-I:%M%P')
- if event.visible_to_user?(current_user)
- if event.push_action?
@@ -20,7 +20,7 @@
- if event.note?
= link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title
- elsif event.target
- = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
+ = link_to event.target.to_reference, [event.project, event.target], class: 'has-tooltip', title: event.target_title
= s_('UserProfile|at')
%strong
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index d2f7ff91f0d..e1d1df9de1a 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -14,7 +14,7 @@
= render layout: 'users/cover_controls' do
- if @user == current_user
= link_to profile_path, class: link_classes + 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do
- = icon('pencil')
+ = sprite_icon('pencil')
- elsif current_user
- if @user.abuse_report
%button{ class: link_classes + 'btn btn-danger mr-1', title: s_('UserProfile|Already reported for abuse'),
@@ -25,8 +25,8 @@
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('exclamation-circle')
- if can?(current_user, :read_user_profile, @user)
- = link_to user_path(@user, rss_url_options), class: link_classes + 'btn btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
- = icon('rss')
+ = link_to user_path(@user, rss_url_options), class: link_classes + 'btn btn-svg btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
+ = sprite_icon('rss', css_class: 'qa-rss-icon')
- if current_user && current_user.admin?
= link_to [:admin, @user], class: link_classes + 'btn btn-default', title: s_('UserProfile|View user in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
@@ -55,12 +55,12 @@
.cover-desc.cgray.mb-1.mb-sm-2
- unless @user.location.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mb-1.mb-sm-0
- = sprite_icon('location', size: 16, css_class: 'vertical-align-sub fgray')
+ = sprite_icon('location', css_class: 'vertical-align-sub fgray')
%span.vertical-align-middle
= @user.location
- unless work_information(@user).blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline
- = sprite_icon('work', size: 16, css_class: 'vertical-align-middle fgray')
+ = sprite_icon('work', css_class: 'vertical-align-middle fgray')
%span.vertical-align-middle
= work_information(@user)
.cover-desc.cgray.mb-1.mb-sm-2
@@ -84,7 +84,7 @@
= link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link'
- if @user.bio.present?
.cover-desc.cgray
- %p.profile-user-bio
+ .profile-user-bio
= markdown(@user.bio_html)
@@ -160,16 +160,12 @@
.loading.hide
.spinner.spinner-md
- - if profile_tabs.empty?
- .row
- .col-12
- .svg-content
- = image_tag 'illustrations/profile_private_mode.svg'
- .col-12.text-center
- .text-content
- %h4
- - if @user.blocked?
- = s_('UserProfile|This user is blocked')
- - else
- = s_('UserProfile|This user has a private profile')
-
+ - if profile_tabs.empty?
+ .svg-content
+ = image_tag 'illustrations/profile_private_mode.svg'
+ .text-content.text-center
+ %h4
+ - if @user.blocked?
+ = s_('UserProfile|This user is blocked')
+ - else
+ = s_('UserProfile|This user has a private profile')
diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml
index bc4861d6ae8..175fde1c862 100644
--- a/app/views/users/terms/index.html.haml
+++ b/app/views/users/terms/index.html.haml
@@ -9,7 +9,7 @@
= button_to accept_term_path(@term, redirect_params), class: 'btn btn-success gl-ml-3', data: { qa_selector: 'accept_terms_button' } do
= _('Accept terms')
- else
- .pull-right
+ .float-right
= link_to root_path, class: 'btn btn-success gl-ml-3' do
= _('Continue')
- if can?(current_user, :decline_terms, @term)
diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb
index c84ac60d777..8d589c03259 100644
--- a/app/workers/admin_email_worker.rb
+++ b/app/workers/admin_email_worker.rb
@@ -18,7 +18,7 @@ class AdminEmailWorker # rubocop:disable Scalability/IdempotentWorker
# rubocop: disable CodeReuse/ActiveRecord
def send_repository_check_mail
repository_check_failed_count = Project.where(last_repository_check_failed: true).count
- return if repository_check_failed_count.zero?
+ return if repository_check_failed_count == 0
RepositoryCheckMailer.notify(repository_check_failed_count).deliver_now
end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 5148772c881..2c871c55f0a 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -5,7 +5,7 @@
---
- :name: authorized_project_update:authorized_project_update_project_create
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -13,7 +13,7 @@
:tags: []
- :name: authorized_project_update:authorized_project_update_project_group_link_create
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -21,7 +21,7 @@
:tags: []
- :name: authorized_project_update:authorized_project_update_user_refresh_over_user_range
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -29,7 +29,7 @@
:tags: []
- :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -37,87 +37,87 @@
:tags: []
- :name: auto_devops:auto_devops_disable
:feature_category: :auto_devops
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: auto_merge:auto_merge_process
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: chaos:chaos_cpu_spin
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: chaos:chaos_db_spin
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: chaos:chaos_kill
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: chaos:chaos_leak_mem
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: chaos:chaos_sleep
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: container_repository:cleanup_container_repository
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: container_repository:delete_container_repository
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:admin_email
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:authorized_project_update_periodic_recalculate
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -125,79 +125,79 @@
:tags: []
- :name: cronjob:ci_archive_traces_cron
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:container_expiration_policy
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:environments_auto_stop_cron
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:expire_build_artifacts
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:gitlab_usage_ping
:feature_category: :collection
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:import_export_project_cleanup
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:import_stuck_project_import_jobs
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:issue_due_scheduler
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:jira_import_stuck_jira_import_jobs
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:metrics_dashboard_schedule_annotations_prune
:feature_category: :metrics
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -205,167 +205,175 @@
:tags: []
- :name: cronjob:namespaces_prune_aggregation_schedules
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:pages_domain_removal_cron
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:pages_domain_ssl_renewal_cron
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:pages_domain_verification_cron
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:partition_creation
:feature_category: :database
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
+- :name: cronjob:personal_access_tokens_expired_notification
+ :feature_category: :authentication_and_authorization
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: cronjob:personal_access_tokens_expiring
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:pipeline_schedule
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:prune_old_events
:feature_category: :users
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:prune_web_hook_logs
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:remove_expired_group_links
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:remove_expired_members
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:remove_unreferenced_lfs_objects
:feature_category: :git_lfs
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:repository_archive_cache
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:repository_check_dispatch
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:requests_profiles
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:schedule_migrate_external_diffs
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:stuck_ci_jobs
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:stuck_export_jobs
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:stuck_merge_jobs
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:trending_projects
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:update_container_registry_info
:feature_category: :container_registry
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -373,11 +381,11 @@
:tags: []
- :name: cronjob:users_create_statistics
:feature_category: :users
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: cronjob:x509_issuer_crl_check
:feature_category: :source_code_management
@@ -389,27 +397,27 @@
:tags: []
- :name: deployment:deployments_finished
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: deployment:deployments_forward_deployment
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: deployment:deployments_success
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:cluster_configure_istio
:feature_category: :kubernetes_management
@@ -417,7 +425,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:cluster_install_app
:feature_category: :kubernetes_management
@@ -425,7 +433,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:cluster_patch_app
:feature_category: :kubernetes_management
@@ -433,7 +441,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:cluster_provision
:feature_category: :kubernetes_management
@@ -441,15 +449,15 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:cluster_update_app
:feature_category: :kubernetes_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:cluster_upgrade_app
:feature_category: :kubernetes_management
@@ -457,7 +465,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:cluster_wait_for_app_installation
:feature_category: :kubernetes_management
@@ -465,15 +473,15 @@
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:cluster_wait_for_app_update
:feature_category: :kubernetes_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:cluster_wait_for_ingress_ip_address
:feature_category: :kubernetes_management
@@ -481,23 +489,23 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:clusters_applications_activate_service
:feature_category: :kubernetes_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:clusters_applications_deactivate_service
:feature_category: :kubernetes_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:clusters_applications_uninstall
:feature_category: :kubernetes_management
@@ -505,7 +513,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:clusters_applications_wait_for_uninstall_app
:feature_category: :kubernetes_management
@@ -513,7 +521,7 @@
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:clusters_cleanup_app
:feature_category: :kubernetes_management
@@ -521,7 +529,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:clusters_cleanup_project_namespace
:feature_category: :kubernetes_management
@@ -529,7 +537,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:clusters_cleanup_service_account
:feature_category: :kubernetes_management
@@ -537,7 +545,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gcp_cluster:wait_for_cluster_creation
:feature_category: :kubernetes_management
@@ -545,7 +553,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_import_diff_note
:feature_category: :importers
@@ -553,7 +561,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_import_issue
:feature_category: :importers
@@ -561,7 +569,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_import_lfs_object
:feature_category: :importers
@@ -569,7 +577,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_import_note
:feature_category: :importers
@@ -577,7 +585,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_import_pull_request
:feature_category: :importers
@@ -585,103 +593,103 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_refresh_import_jid
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_stage_finish_import
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_base_data
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_issues_and_diff_notes
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_lfs_objects
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_notes
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_pull_requests
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_importer:github_import_stage_import_repository
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: hashed_storage:hashed_storage_migrator
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: hashed_storage:hashed_storage_project_migrate
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: hashed_storage:hashed_storage_project_rollback
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: hashed_storage:hashed_storage_rollbacker
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: incident_management:clusters_applications_check_prometheus_health
:feature_category: :incident_management
@@ -693,175 +701,175 @@
:tags: []
- :name: incident_management:incident_management_pager_duty_process_incident
:feature_category: :incident_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: incident_management:incident_management_process_alert
:feature_category: :incident_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: incident_management:incident_management_process_prometheus_alert
:feature_category: :incident_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: jira_importer:jira_import_advance_stage
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: jira_importer:jira_import_import_issue
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_finish_import
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_import_attachments
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_import_issues
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_import_labels
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_import_notes
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: jira_importer:jira_import_stage_start_import
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: mail_scheduler:mail_scheduler_issue_due
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: mail_scheduler:mail_scheduler_notification_service
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: object_pool:object_pool_create
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: object_pool:object_pool_destroy
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: object_pool:object_pool_join
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: object_pool:object_pool_schedule_join
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: object_storage:object_storage_background_move
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: object_storage:object_storage_migrate_uploads
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: package_repositories:packages_nuget_extraction
:feature_category: :package_registry
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_background:archive_trace
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_background:ci_build_report_result
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -869,15 +877,15 @@
:tags: []
- :name: pipeline_background:ci_build_trace_chunk_flush
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_background:ci_daily_build_group_report_results
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -885,7 +893,7 @@
:tags: []
- :name: pipeline_background:ci_pipeline_success_unlock_artifacts
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -893,7 +901,7 @@
:tags: []
- :name: pipeline_background:ci_ref_delete_unlock_artifacts
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -901,7 +909,7 @@
:tags: []
- :name: pipeline_cache:expire_job_cache
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 3
@@ -909,7 +917,7 @@
:tags: []
- :name: pipeline_cache:expire_pipeline_cache
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 3
@@ -917,152 +925,152 @@
:tags: []
- :name: pipeline_creation:create_pipeline
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 4
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_creation:run_pipeline_schedule
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 4
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_default:build_coverage
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_default:build_trace_sections
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_default:ci_create_cross_project_pipeline
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_default:ci_pipeline_bridge_status
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_default:pipeline_metrics
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_default:pipeline_notification
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_default:pipeline_update_ci_ref_status
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_hooks:build_hooks
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_hooks:pipeline_hooks
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_processing:build_finished
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 5
- :idempotent:
+ :idempotent:
:tags:
- :requires_disk_io
- :name: pipeline_processing:build_queue
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 5
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_processing:build_success
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 5
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_processing:ci_build_prepare
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 5
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_processing:ci_build_schedule
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 5
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_processing:ci_resource_groups_assign_resource_from_resource_group
:feature_category: :continuous_delivery
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 5
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_processing:pipeline_process
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 5
- :idempotent:
+ :idempotent:
:tags: []
- :name: pipeline_processing:pipeline_update
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 5
@@ -1070,7 +1078,7 @@
:tags: []
- :name: pipeline_processing:stage_update
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 5
@@ -1078,7 +1086,7 @@
:tags: []
- :name: pipeline_processing:update_head_pipeline_for_merge_request
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 5
@@ -1086,71 +1094,71 @@
:tags: []
- :name: repository_check:repository_check_batch
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: repository_check:repository_check_clear
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: repository_check:repository_check_single_repository
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_confidential_issue
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_entity_leave
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_group_private
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_private_features
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: todos_destroyer:todos_destroyer_project_private
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: unassign_issuables:members_destroyer_unassign_issuables
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1158,7 +1166,7 @@
:tags: []
- :name: update_namespace_statistics:namespaces_root_statistics
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1166,7 +1174,7 @@
:tags: []
- :name: update_namespace_statistics:namespaces_schedule_aggregation
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1174,7 +1182,7 @@
:tags: []
- :name: authorized_keys
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 2
@@ -1182,7 +1190,7 @@
:tags: []
- :name: authorized_projects
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 2
@@ -1190,11 +1198,11 @@
:tags: []
- :name: background_migration
:feature_category: :database
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: chat_notification
:feature_category: :chatops
@@ -1202,11 +1210,11 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: create_commit_signature
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
@@ -1214,91 +1222,91 @@
:tags: []
- :name: create_evidence
:feature_category: :release_evidence
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: create_note_diff_file
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: default
- :feature_category:
- :has_external_dependencies:
- :urgency:
- :resource_boundary:
+ :feature_category:
+ :has_external_dependencies:
+ :urgency:
+ :resource_boundary:
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: delete_diff_files
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: delete_merged_branches
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: delete_stored_files
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: delete_user
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: design_management_new_version
:feature_category: :design_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :memory
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: detect_repository_languages
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: email_receiver
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: emails_on_push
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: error_tracking_issue_link
:feature_category: :error_tracking
@@ -1306,23 +1314,23 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: expire_build_instance_artifacts
:feature_category: :continuous_integration
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: export_csv
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: external_service_reactive_caching
:feature_category: :not_owned
@@ -1330,99 +1338,107 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: file_hook
:feature_category: :integrations
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
+- :name: flush_counter_increments
+ :feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: true
:tags: []
- :name: git_garbage_collect
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: github_import_advance_stage
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: gitlab_shell
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: group_destroy
:feature_category: :subgroups
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: group_export
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: group_import
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: import_issues_csv
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: invalid_gpg_signature_update
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: irker
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: mailers
- :feature_category:
- :has_external_dependencies:
- :urgency:
- :resource_boundary:
+ :feature_category:
+ :has_external_dependencies:
+ :urgency:
+ :resource_boundary:
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: merge
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 5
@@ -1430,7 +1446,7 @@
:tags: []
- :name: merge_request_mergeability_check
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1438,7 +1454,7 @@
:tags: []
- :name: metrics_dashboard_prune_old_annotations
:feature_category: :metrics
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
@@ -1446,87 +1462,95 @@
:tags: []
- :name: migrate_external_diffs
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: namespaceless_project_destroy
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: new_issue
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: new_merge_request
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: new_note
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: pages
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: pages_domain_ssl_renewal
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: pages_domain_verification
:feature_category: :pages
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
+ :tags: []
+- :name: pages_update_configuration
+ :feature_category: :pages
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
:tags: []
- :name: phabricator_import_import_tasks
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: post_receive
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 5
- :idempotent:
+ :idempotent:
:tags: []
- :name: process_commit
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 3
@@ -1534,35 +1558,35 @@
:tags: []
- :name: project_cache
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: project_daily_statistics
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: project_destroy
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: project_export
:feature_category: :importers
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :throttled
:resource_boundary: :memory
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: project_service
:feature_category: :integrations
@@ -1570,11 +1594,11 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: project_update_repository_storage
:feature_category: :gitaly
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
@@ -1582,7 +1606,7 @@
:tags: []
- :name: prometheus_create_default_alerts
:feature_category: :incident_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 1
@@ -1590,59 +1614,59 @@
:tags: []
- :name: propagate_integration
:feature_category: :integrations
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: propagate_service_template
- :feature_category: :source_code_management
- :has_external_dependencies:
+ :feature_category: :integrations
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: reactive_caching
:feature_category: :not_owned
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: rebase
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: remote_mirror_notification
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: repository_cleanup
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: repository_fork
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: repository_import
:feature_category: :importers
@@ -1650,15 +1674,15 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: repository_remove_remote
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: repository_update_remote_mirror
:feature_category: :source_code_management
@@ -1666,51 +1690,51 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: self_monitoring_project_create
:feature_category: :metrics
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: self_monitoring_project_delete
:feature_category: :metrics
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent:
:tags: []
- :name: service_desk_email_receiver
:feature_category: :issue_tracking
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: system_hook_push
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: update_external_pull_requests
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: update_highest_role
:feature_category: :authentication_and_authorization
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :unknown
:weight: 2
@@ -1718,27 +1742,27 @@
:tags: []
- :name: update_merge_requests
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 3
- :idempotent:
+ :idempotent:
:tags: []
- :name: update_project_statistics
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: upload_checksum
:feature_category: :geo_replication
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: web_hook
:feature_category: :integrations
@@ -1746,11 +1770,11 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent:
:tags: []
- :name: x509_certificate_revoke
:feature_category: :source_code_management
- :has_external_dependencies:
+ :has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
index 9c942228111..30dec5159a2 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -10,6 +10,7 @@ module ApplicationWorker
include Sidekiq::Worker # rubocop:disable Cop/IncludeSidekiqWorker
include WorkerAttributes
include WorkerContext
+ include Gitlab::SidekiqVersioning::Worker
LOGGING_EXTRA_KEY = 'extra'
diff --git a/app/workers/flush_counter_increments_worker.rb b/app/workers/flush_counter_increments_worker.rb
new file mode 100644
index 00000000000..b7e3c0c134d
--- /dev/null
+++ b/app/workers/flush_counter_increments_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# Invoked by CounterAttribute concern when incrementing counter
+# attributes. The method `flush_increments_to_database!` that
+# this worker uses is itself idempotent as it runs with exclusive
+# lease to ensure that only one instance at the time can flush
+# increments from Redis to the database.
+class FlushCounterIncrementsWorker
+ include ApplicationWorker
+
+ feature_category_not_owned!
+ urgency :low
+ deduplicate :until_executing, including_scheduled: true
+
+ idempotent!
+
+ def perform(model_name, model_id, attribute)
+ return unless self.class.const_defined?(model_name)
+
+ model_class = model_name.constantize
+ model = model_class.find_by_id(model_id)
+ return unless model
+
+ model.flush_increments_to_database!(attribute)
+ end
+end
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index f2222c7be5e..6e4feea1b26 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -11,6 +11,7 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
LEASE_TIMEOUT = 86400
def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil)
+ lease_key ||= "git_gc:#{task}:#{project_id}"
project = Project.find(project_id)
active_uuid = get_lease_uuid(lease_key)
@@ -26,14 +27,20 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
task = task.to_sym
- ::Projects::GitDeduplicationService.new(project).execute if task == :gc
+ if task == :gc
+ ::Projects::GitDeduplicationService.new(project).execute
+ cleanup_orphan_lfs_file_references(project)
+ end
gitaly_call(task, project.repository.raw_repository)
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
- project.repository.expire_statistics_caches if task != :pack_refs
+ if task != :pack_refs
+ project.repository.expire_statistics_caches
+ Projects::UpdateStatisticsService.new(project, nil, statistics: [:repository_size, :lfs_objects_size]).execute
+ end
# In case pack files are deleted, release libgit2 cache and open file
# descriptors ASAP instead of waiting for Ruby garbage collection
@@ -86,6 +93,13 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
raise Gitlab::Git::CommandError.new(e)
end
+ def cleanup_orphan_lfs_file_references(project)
+ return unless Feature.enabled?(:cleanup_lfs_during_gc, project)
+ return if Gitlab::Database.read_only? # GitGarbageCollectWorker may be run on a Geo secondary
+
+ ::Gitlab::Cleanup::OrphanLfsFileReferences.new(project, dry_run: false, logger: logger).run!
+ end
+
def flush_ref_caches(project)
project.repository.after_create_branch
project.repository.branch_names
diff --git a/app/workers/gitlab/import/advance_stage.rb b/app/workers/gitlab/import/advance_stage.rb
index 3f34437294e..9fc03efe9d0 100644
--- a/app/workers/gitlab/import/advance_stage.rb
+++ b/app/workers/gitlab/import/advance_stage.rb
@@ -41,7 +41,7 @@ module Gitlab
# complete the work fast enough.
waiter.wait(BLOCKING_WAIT_TIME)
- next unless waiter.jobs_remaining.positive?
+ next unless waiter.jobs_remaining > 0
new_waiters[waiter.key] = waiter.jobs_remaining
end
diff --git a/app/workers/gitlab_usage_ping_worker.rb b/app/workers/gitlab_usage_ping_worker.rb
index 9f0cf1728dd..a696c6e746a 100644
--- a/app/workers/gitlab_usage_ping_worker.rb
+++ b/app/workers/gitlab_usage_ping_worker.rb
@@ -1,32 +1,24 @@
# frozen_string_literal: true
class GitlabUsagePingWorker # rubocop:disable Scalability/IdempotentWorker
+ LEASE_KEY = 'gitlab_usage_ping_worker:ping'
LEASE_TIMEOUT = 86400
include ApplicationWorker
- # rubocop:disable Scalability/CronWorkerContext
- # This worker does not perform work scoped to a context
- include CronjobQueue
- # rubocop:enable Scalability/CronWorkerContext
+ include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+ include Gitlab::ExclusiveLeaseHelpers
feature_category :collection
-
- # Retry for up to approximately three hours then give up.
- sidekiq_options retry: 10, dead: false
+ sidekiq_options retry: 3, dead: false
+ sidekiq_retry_in { |count| (count + 1) * 8.hours.to_i }
def perform
# Multiple Sidekiq workers could run this. We should only do this at most once a day.
- return unless try_obtain_lease
-
- # Splay the request over a minute to avoid thundering herd problems.
- sleep(rand(0.0..60.0).round(3))
-
- SubmitUsagePingService.new.execute
- end
-
- private
+ in_lock(LEASE_KEY, ttl: LEASE_TIMEOUT) do
+ # Splay the request over a minute to avoid thundering herd problems.
+ sleep(rand(0.0..60.0).round(3))
- def try_obtain_lease
- Gitlab::ExclusiveLease.new('gitlab_usage_ping_worker:ping', timeout: LEASE_TIMEOUT).try_obtain
+ SubmitUsagePingService.new.execute
+ end
end
end
diff --git a/app/workers/incident_management/process_alert_worker.rb b/app/workers/incident_management/process_alert_worker.rb
index 5d5c10014f8..59464b81d1b 100644
--- a/app/workers/incident_management/process_alert_worker.rb
+++ b/app/workers/incident_management/process_alert_worker.rb
@@ -16,10 +16,10 @@ module IncidentManagement
alert = find_alert(alert_id)
return unless alert
- new_issue = create_issue_for(alert)
- return unless new_issue&.persisted?
+ result = create_issue_for(alert)
+ return if result.success?
- link_issue_with_alert(alert, new_issue.id)
+ log_warning(alert, result)
end
private
@@ -28,29 +28,20 @@ module IncidentManagement
AlertManagement::Alert.find_by_id(alert_id)
end
- def parsed_payload(alert)
- if alert.prometheus?
- alert.payload
- else
- Gitlab::Alerting::NotificationPayloadParser.call(alert.payload.to_h, alert.project)
- end
- end
-
def create_issue_for(alert)
- IncidentManagement::CreateIssueService
- .new(alert.project, parsed_payload(alert))
+ AlertManagement::CreateAlertIssueService
+ .new(alert, User.alert_bot)
.execute
- .dig(:issue)
end
- def link_issue_with_alert(alert, issue_id)
- return if alert.update(issue_id: issue_id)
+ def log_warning(alert, result)
+ issue_id = result.payload[:issue]&.id
Gitlab::AppLogger.warn(
- message: 'Cannot link an Issue with Alert',
+ message: 'Cannot process an Incident',
issue_id: issue_id,
alert_id: alert.id,
- alert_errors: alert.errors.messages
+ errors: result.message
)
end
end
diff --git a/app/workers/pages_update_configuration_worker.rb b/app/workers/pages_update_configuration_worker.rb
new file mode 100644
index 00000000000..d0904db6b42
--- /dev/null
+++ b/app/workers/pages_update_configuration_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class PagesUpdateConfigurationWorker
+ include ApplicationWorker
+
+ idempotent!
+ feature_category :pages
+
+ def perform(project_id)
+ project = Project.find_by_id(project_id)
+ return unless project
+
+ result = Projects::UpdatePagesConfigurationService.new(project).execute
+
+ # The ConfigurationService swallows all exceptions and wraps them in a status
+ # we need to keep this while the feature flag still allows running this
+ # service within a request.
+ # But we might as well take advantage of sidekiq retries here.
+ # We should let the service raise after we remove the feature flag
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/230695
+ raise result[:exception] if result[:exception]
+ end
+end
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index d699e32c1a0..aefa4bc4223 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -14,12 +14,10 @@ class PagesWorker # rubocop:disable Scalability/IdempotentWorker
# rubocop: disable CodeReuse/ActiveRecord
def deploy(build_id)
build = Ci::Build.find_by(id: build_id)
- result = Projects::UpdatePagesService.new(build.project, build).execute
- if result[:status] == :success
- result = Projects::UpdatePagesConfigurationService.new(build.project).execute
+ update_contents = Projects::UpdatePagesService.new(build.project, build).execute
+ if update_contents[:status] == :success
+ Projects::UpdatePagesConfigurationService.new(build.project).execute
end
-
- result
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/partition_creation_worker.rb b/app/workers/partition_creation_worker.rb
index 9101623d93a..b833e818b32 100644
--- a/app/workers/partition_creation_worker.rb
+++ b/app/workers/partition_creation_worker.rb
@@ -8,8 +8,6 @@ class PartitionCreationWorker
idempotent!
def perform
- Gitlab::AppLogger.info("Checking state of dynamic postgres partitions")
-
Gitlab::Database::Partitioning::PartitionCreator.new.create_partitions
end
end
diff --git a/app/workers/personal_access_tokens/expired_notification_worker.rb b/app/workers/personal_access_tokens/expired_notification_worker.rb
new file mode 100644
index 00000000000..c1b1f1a461d
--- /dev/null
+++ b/app/workers/personal_access_tokens/expired_notification_worker.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module PersonalAccessTokens
+ class ExpiredNotificationWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ feature_category :authentication_and_authorization
+
+ def perform(*args)
+ return unless Feature.enabled?(:expired_pat_email_notification)
+
+ notification_service = NotificationService.new
+
+ User.with_personal_access_tokens_expired_today.find_each do |user|
+ with_context(user: user) do
+ Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about an expired token"
+
+ notification_service.access_token_expired(user)
+
+ user.personal_access_tokens.without_impersonation.expired_today_and_not_notified.update_all(after_expiry_notification_delivered: true)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index cd7c82d3117..f0929b92bd0 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -10,11 +10,13 @@ class PipelineProcessWorker # rubocop:disable Scalability/IdempotentWorker
loggable_arguments 1
# rubocop: disable CodeReuse/ActiveRecord
- def perform(pipeline_id, build_ids = nil)
+ # `_build_ids` is deprecated and will be removed in 14.0
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/232806
+ def perform(pipeline_id, _build_ids = nil)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
Ci::ProcessPipelineService
.new(pipeline)
- .execute(build_ids)
+ .execute
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index 267caa5bedd..7db4ab8fe0b 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# This worker is deprecated and will be removed in 14.0
+# See: https://gitlab.com/gitlab-org/gitlab/-/issues/232806
class PipelineUpdateWorker
include ApplicationWorker
include PipelineQueue
@@ -9,7 +11,7 @@ class PipelineUpdateWorker
idempotent!
- def perform(pipeline_id)
- Ci::Pipeline.find_by_id(pipeline_id)&.update_legacy_status
+ def perform(_pipeline_id)
+ # no-op
end
end
diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb
index f3a6bda1821..37d5ccb656d 100644
--- a/app/workers/propagate_service_template_worker.rb
+++ b/app/workers/propagate_service_template_worker.rb
@@ -4,7 +4,7 @@
class PropagateServiceTemplateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- feature_category :source_code_management
+ feature_category :integrations
LEASE_TIMEOUT = 4.hours.to_i