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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue4
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue (renamed from app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue)39
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue494
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue81
-rw-r--r--app/assets/javascripts/alerts_settings/services/index.js21
-rw-r--r--app/assets/javascripts/behaviors/select2.js33
-rw-r--r--app/assets/javascripts/blob/file_template_selector.js15
-rw-r--r--app/assets/javascripts/blob/template_selector.js11
-rw-r--r--app/assets/javascripts/boards/boards_util.js36
-rw-r--r--app/assets/javascripts/boards/components/board_assignee_dropdown.vue99
-rw-r--r--app/assets/javascripts/boards/components/board_column_new.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_new.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_list_new.vue101
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue16
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue15
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue3
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue161
-rw-r--r--app/assets/javascripts/boards/queries/group_milestones.query.graphql17
-rw-r--r--app/assets/javascripts/boards/queries/issue.fragment.graphql4
-rw-r--r--app/assets/javascripts/boards/queries/issue_set_milestone.mutation.graphql12
-rw-r--r--app/assets/javascripts/boards/stores/actions.js31
-rw-r--r--app/assets/javascripts/clone_panel.js42
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js7
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue36
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue9
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue2
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js6
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js27
-rw-r--r--app/assets/javascripts/diffs/components/app.vue3
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue10
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue18
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue12
-rw-r--r--app/assets/javascripts/diffs/store/actions.js90
-rw-r--r--app/assets/javascripts/diffs/store/getters.js48
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js80
-rw-r--r--app/assets/javascripts/diffs/store/utils.js186
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue67
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue30
-rw-r--r--app/assets/javascripts/groups/index.js3
-rw-r--r--app/assets/javascripts/groups/members/components/app.vue4
-rw-r--r--app/assets/javascripts/groups/members/index.js8
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js5
-rw-r--r--app/assets/javascripts/groups/store/utils.js27
-rw-r--r--app/assets/javascripts/groups_select.js178
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue71
-rw-r--r--app/assets/javascripts/ide/components/ide.vue19
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue16
-rw-r--r--app/assets/javascripts/ide/components/nav_dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue20
-rw-r--r--app/assets/javascripts/ide/components/terminal/session.vue18
-rw-r--r--app/assets/javascripts/ide/ide_router.js16
-rw-r--r--app/assets/javascripts/ide/index.js5
-rw-r--r--app/assets/javascripts/ide/stores/actions.js21
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js23
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js21
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js6
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js14
-rw-r--r--app/assets/javascripts/issuable_context.js14
-rw-r--r--app/assets/javascripts/issuable_form.js64
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue180
-rw-r--r--app/assets/javascripts/issue_show/utils/parse_data.js2
-rw-r--r--app/assets/javascripts/jira_connect/components/app.vue4
-rw-r--r--app/assets/javascripts/jobs/utils.js14
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js45
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js8
-rw-r--r--app/assets/javascripts/lib/utils/scroll_utils.js2
-rw-r--r--app/assets/javascripts/logs/utils.js2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/access_request_action_buttons.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/action_button_group.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/action_button_group.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/approve_access_request_button.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/group_action_buttons.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/invite_action_buttons.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/leave_button.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/leave_button.vue)2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/remove_group_link_button.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/remove_member_button.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/resend_invite_button.vue)0
-rw-r--r--app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue (renamed from app/assets/javascripts/vue_shared/components/members/action_buttons/user_action_buttons.vue)2
-rw-r--r--app/assets/javascripts/members/components/avatars/group_avatar.vue (renamed from app/assets/javascripts/vue_shared/components/members/avatars/group_avatar.vue)2
-rw-r--r--app/assets/javascripts/members/components/avatars/invite_avatar.vue (renamed from app/assets/javascripts/vue_shared/components/members/avatars/invite_avatar.vue)2
-rw-r--r--app/assets/javascripts/members/components/avatars/user_avatar.vue (renamed from app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue)4
-rw-r--r--app/assets/javascripts/members/components/modals/leave_modal.vue (renamed from app/assets/javascripts/vue_shared/components/members/modals/leave_modal.vue)2
-rw-r--r--app/assets/javascripts/members/components/modals/remove_group_link_modal.vue (renamed from app/assets/javascripts/vue_shared/components/members/modals/remove_group_link_modal.vue)2
-rw-r--r--app/assets/javascripts/members/components/table/created_at.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/created_at.vue)0
-rw-r--r--app/assets/javascripts/members/components/table/expiration_datepicker.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/expiration_datepicker.vue)0
-rw-r--r--app/assets/javascripts/members/components/table/expires_at.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/expires_at.vue)2
-rw-r--r--app/assets/javascripts/members/components/table/member_action_buttons.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/member_action_buttons.vue)2
-rw-r--r--app/assets/javascripts/members/components/table/member_avatar.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue)0
-rw-r--r--app/assets/javascripts/members/components/table/member_source.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/member_source.vue)0
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/members_table.vue)15
-rw-r--r--app/assets/javascripts/members/components/table/members_table_cell.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue)11
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue (renamed from app/assets/javascripts/vue_shared/components/members/table/role_dropdown.vue)3
-rw-r--r--app/assets/javascripts/members/constants.js (renamed from app/assets/javascripts/vue_shared/components/members/constants.js)0
-rw-r--r--app/assets/javascripts/members/store/actions.js (renamed from app/assets/javascripts/vuex_shared/modules/members/actions.js)0
-rw-r--r--app/assets/javascripts/members/store/index.js9
-rw-r--r--app/assets/javascripts/members/store/mutation_types.js (renamed from app/assets/javascripts/vuex_shared/modules/members/mutation_types.js)0
-rw-r--r--app/assets/javascripts/members/store/mutations.js (renamed from app/assets/javascripts/vuex_shared/modules/members/mutations.js)0
-rw-r--r--app/assets/javascripts/members/store/state.js (renamed from app/assets/javascripts/vuex_shared/modules/members/state.js)0
-rw-r--r--app/assets/javascripts/members/store/utils.js (renamed from app/assets/javascripts/vuex_shared/modules/members/utils.js)0
-rw-r--r--app/assets/javascripts/members/utils.js (renamed from app/assets/javascripts/vue_shared/components/members/utils.js)0
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js1
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/variable_mapping.js2
-rw-r--r--app/assets/javascripts/monitoring/utils.js2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue9
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js9
-rw-r--r--app/assets/javascripts/notes/stores/actions.js19
-rw-r--r--app/assets/javascripts/notes/stores/collapse_utils.js3
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/javascripts/packages/list/constants.js4
-rw-r--r--app/assets/javascripts/packages/shared/constants.js1
-rw-r--r--app/assets/javascripts/packages/shared/utils.js3
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js28
-rw-r--r--app/assets/javascripts/pages/projects/commit/pipelines/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js52
-rw-r--r--app/assets/javascripts/pages/projects/commits/show/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js52
-rw-r--r--app/assets/javascripts/pages/projects/project.js40
-rw-r--r--app/assets/javascripts/pages/projects/settings/access_tokens/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js2
-rw-r--r--app/assets/javascripts/performance/constants.js21
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue139
-rw-r--r--app/assets/javascripts/pipeline_editor/components/text_editor.vue14
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql26
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js12
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue201
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue26
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js1
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js7
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js1
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutations.js8
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/state.js4
-rw-r--r--app/assets/javascripts/project_select.js196
-rw-r--r--app/assets/javascripts/project_select_combo_button.js12
-rw-r--r--app/assets/javascripts/projects/experiment_new_project_creation/components/app.vue6
-rw-r--r--app/assets/javascripts/registry/explorer/pages/index.vue4
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_dropdown.vue50
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_run_text.vue31
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_textarea.vue109
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_toggle.vue55
-rw-r--r--app/assets/javascripts/registry/settings/constants.js45
-rw-r--r--app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql1
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js13
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue8
-rw-r--r--app/assets/javascripts/reports/constants.js14
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue14
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue14
-rw-r--r--app/assets/javascripts/users_select/index.js172
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue166
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js142
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_mentions.vue238
-rw-r--r--app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/select2_select.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue3
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/constants.js8
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue59
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue150
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/constants.js7
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/getters.js66
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/index.js16
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/messages.js4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/state.js5
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/utils.js78
-rw-r--r--app/assets/javascripts/vuex_shared/modules/members/index.js10
-rw-r--r--app/assets/javascripts/vulnerabilities/constants.js15
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/framework/awards.scss108
-rw-r--r--app/assets/stylesheets/framework/common.scss5
-rw-r--r--app/assets/stylesheets/framework/diffs.scss4
-rw-r--r--app/assets/stylesheets/framework/forms.scss5
-rw-r--r--app/assets/stylesheets/framework/selects.scss288
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/ci_status.scss1
-rw-r--r--app/assets/stylesheets/pages/clusters.scss16
-rw-r--r--app/assets/stylesheets/pages/editor.scss4
-rw-r--r--app/assets/stylesheets/pages/import.scss8
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/issues.scss5
-rw-r--r--app/assets/stylesheets/pages/projects.scss16
-rw-r--r--app/controllers/admin/cohorts_controller.rb2
-rw-r--r--app/controllers/admin/dashboard_controller.rb5
-rw-r--r--app/controllers/admin/instance_review_controller.rb2
-rw-r--r--app/controllers/admin/instance_statistics_controller.rb2
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/concerns/sorting_preference.rb27
-rw-r--r--app/controllers/concerns/wiki_actions.rb24
-rw-r--r--app/controllers/groups/application_controller.rb13
-rw-r--r--app/controllers/groups/children_controller.rb10
-rw-r--r--app/controllers/groups/milestones_controller.rb3
-rw-r--r--app/controllers/groups_controller.rb5
-rw-r--r--app/controllers/projects/alert_management_controller.rb2
-rw-r--r--app/controllers/projects/alerting/notifications_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb5
-rw-r--r--app/controllers/projects/milestones_controller.rb3
-rw-r--r--app/controllers/projects/prometheus/alerts_controller.rb2
-rw-r--r--app/controllers/projects/settings/operations_controller.rb1
-rw-r--r--app/controllers/projects_controller.rb6
-rw-r--r--app/controllers/repositories/lfs_api_controller.rb22
-rw-r--r--app/controllers/uploads_controller.rb8
-rw-r--r--app/finders/issuable_finder.rb9
-rw-r--r--app/finders/merge_requests_finder.rb32
-rw-r--r--app/finders/merge_requests_finder/params.rb25
-rw-r--r--app/finders/releases/evidence_pipeline_finder.rb46
-rw-r--r--app/graphql/mutations/alert_management/create_alert_issue.rb1
-rw-r--r--app/graphql/mutations/award_emojis/add.rb2
-rw-r--r--app/graphql/mutations/award_emojis/base.rb21
-rw-r--r--app/graphql/mutations/award_emojis/remove.rb2
-rw-r--r--app/graphql/mutations/award_emojis/toggle.rb2
-rw-r--r--app/graphql/mutations/concerns/mutations/finds_by_gid.rb9
-rw-r--r--app/graphql/mutations/issues/update.rb2
-rw-r--r--app/graphql/mutations/releases/create.rb3
-rw-r--r--app/graphql/mutations/releases/update.rb70
-rw-r--r--app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb2
-rw-r--r--app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb27
-rw-r--r--app/graphql/types/error_tracking/sentry_error_collection_type.rb21
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/graphql/types/project_type.rb3
-rw-r--r--app/graphql/types/user_type.rb2
-rw-r--r--app/helpers/application_settings_helper.rb6
-rw-r--r--app/helpers/blob_helper.rb3
-rw-r--r--app/helpers/button_helper.rb10
-rw-r--r--app/helpers/diff_helper.rb8
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/environment_helper.rb2
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb5
-rw-r--r--app/helpers/storage_helper.rb6
-rw-r--r--app/helpers/suggest_pipeline_helper.rb4
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/helpers/users_helper.rb13
-rw-r--r--app/models/analytics/devops_adoption/segment.rb2
-rw-r--r--app/models/analytics/devops_adoption/segment_selection.rb8
-rw-r--r--app/models/bulk_imports/entity.rb19
-rw-r--r--app/models/ci/bridge.rb2
-rw-r--r--app/models/ci/build.rb20
-rw-r--r--app/models/ci/build_trace_chunks/fog.rb29
-rw-r--r--app/models/ci/job_artifact.rb8
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/clusters/applications/helm.rb35
-rw-r--r--app/models/concerns/enums/internal_id.rb3
-rw-r--r--app/models/concerns/optimized_issuable_label_filter.rb37
-rw-r--r--app/models/deployment.rb17
-rw-r--r--app/models/diff_note.rb4
-rw-r--r--app/models/environment.rb5
-rw-r--r--app/models/exported_protected_branch.rb5
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/merge_request.rb31
-rw-r--r--app/models/merge_request_reviewer.rb2
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/namespace_onboarding_action.rb5
-rw-r--r--app/models/pages/lookup_path.rb33
-rw-r--r--app/models/pages_domain.rb8
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/release.rb2
-rw-r--r--app/models/snippet.rb3
-rw-r--r--app/models/snippet_blob.rb4
-rw-r--r--app/models/system_note_metadata.rb4
-rw-r--r--app/models/terraform/state.rb23
-rw-r--r--app/models/terraform/state_version.rb4
-rw-r--r--app/models/user.rb30
-rw-r--r--app/models/user_callout.rb3
-rw-r--r--app/policies/issuable_policy.rb2
-rw-r--r--app/presenters/projects/import_export/project_export_presenter.rb4
-rw-r--r--app/serializers/merge_request_widget_entity.rb17
-rw-r--r--app/services/issuable/import_csv/base_service.rb8
-rw-r--r--app/services/projects/update_pages_service.rb2
-rw-r--r--app/services/releases/base_service.rb20
-rw-r--r--app/services/releases/create_service.rb22
-rw-r--r--app/services/users/approve_service.rb9
-rw-r--r--app/uploaders/terraform/state_uploader.rb22
-rw-r--r--app/uploaders/terraform/versioned_state_uploader.rb23
-rw-r--r--app/validators/json_schemas/codeclimate.json34
-rw-r--r--app/views/admin/application_settings/_ip_limits.html.haml26
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml8
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/hooks/_form.html.haml8
-rw-r--r--app/views/admin/runners/_sort_dropdown.html.haml2
-rw-r--r--app/views/admin/runners/show.html.haml17
-rw-r--r--app/views/admin/users/_user.html.haml3
-rw-r--r--app/views/admin/users/show.html.haml3
-rw-r--r--app/views/clusters/clusters/show.html.haml1
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml2
-rw-r--r--app/views/groups/_home_panel.html.haml6
-rw-r--r--app/views/groups/_subgroups_and_projects.html.haml2
-rw-r--r--app/views/groups/show.html.haml3
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml9
-rw-r--r--app/views/profiles/notifications/show.html.haml12
-rw-r--r--app/views/projects/_home_panel.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_metrics_dashboard_yml_loading.html.haml2
-rw-r--r--app/views/projects/buttons/_clone.html.haml6
-rw-r--r--app/views/projects/ci/pipeline_editor/show.html.haml2
-rw-r--r--app/views/projects/commit/_verified_signature_badge.html.haml2
-rw-r--r--app/views/projects/commit/x509/_unverified_signature_badge.html.haml2
-rw-r--r--app/views/projects/empty.html.haml3
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml109
-rw-r--r--app/views/projects/merge_requests/conflicts/_commit_stats.html.haml15
-rw-r--r--app/views/projects/merge_requests/conflicts/_file_actions.html.haml16
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml2
-rw-r--r--app/views/projects/merge_requests/conflicts/show.html.haml7
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/no_repo.html.haml4
-rw-r--r--app/views/projects/registry/settings/_index.haml3
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml2
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/show.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tags/new.html.haml2
-rw-r--r--app/views/projects/wikis/git_access.html.haml2
-rw-r--r--app/views/registrations/experience_levels/show.html.haml8
-rw-r--r--app/views/search/_filter.html.haml2
-rw-r--r--app/views/search/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/_alert_info.html.haml6
-rw-r--r--app/views/shared/_clone_panel.html.haml14
-rw-r--r--app/views/shared/_milestones_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/_no_password.html.haml2
-rw-r--r--app/views/shared/_no_ssh.html.haml2
-rw-r--r--app/views/shared/groups/_dropdown.html.haml13
-rw-r--r--app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml2
-rw-r--r--app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml2
-rw-r--r--app/views/shared/labels/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/members/_group.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/shared/projects/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml2
-rw-r--r--app/views/shared/wikis/_form.html.haml2
-rw-r--r--app/views/shared/wikis/_sidebar.html.haml5
-rw-r--r--app/views/shared/wikis/git_error.html.haml14
-rw-r--r--app/workers/all_queues.yml28
-rw-r--r--app/workers/analytics/instance_statistics/count_job_trigger_worker.rb2
-rw-r--r--app/workers/analytics/instance_statistics/counter_job_worker.rb2
-rw-r--r--app/workers/create_evidence_worker.rb20
-rw-r--r--app/workers/releases/create_evidence_worker.rb23
-rw-r--r--app/workers/releases/manage_evidence_worker.rb24
-rw-r--r--app/workers/trending_projects_worker.rb4
369 files changed, 4347 insertions, 3113 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
index 12c0409629f..cf16750dbf8 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
@@ -11,7 +11,6 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Tracking from '~/tracking';
import {
trackAlertIntegrationsViewsOptions,
@@ -54,7 +53,6 @@ export default {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
- mixins: [glFeatureFlagsMixin()],
props: {
integrations: {
type: Array,
@@ -170,7 +168,7 @@ export default {
</template>
<template #cell(actions)="{ item }">
- <gl-button-group v-if="glFeatures.httpIntegrationsList" class="gl-ml-3">
+ <gl-button-group class="gl-ml-3">
<gl-button icon="pencil" @click="$emit('edit-integration', { id: item.id })" />
<gl-button
v-gl-modal.deleteIntegration
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index 3656fc4d7ec..bf2874b6cc7 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -216,8 +216,12 @@ export default {
return {
name: this.currentIntegration?.name || '',
active: this.currentIntegration?.active || false,
- token: this.currentIntegration?.token || this.selectedIntegrationType.token,
- url: this.currentIntegration?.url || this.selectedIntegrationType.url,
+ token:
+ this.currentIntegration?.token ||
+ (this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.token : ''),
+ url:
+ this.currentIntegration?.url ||
+ (this.selectedIntegrationType !== this.generic ? this.selectedIntegrationType.url : ''),
apiUrl: this.currentIntegration?.apiUrl || '',
};
},
@@ -246,8 +250,20 @@ export default {
canEditPayload() {
return this.hasSamplePayload && !this.resetSamplePayloadConfirmed;
},
+ isResetAuthKeyDisabled() {
+ return !this.active && !this.integrationForm.token !== '';
+ },
isPayloadEditDisabled() {
- return !this.active || this.canEditPayload;
+ return this.glFeatures.multipleHttpIntegrationsCustomMapping
+ ? !this.active || this.canEditPayload
+ : !this.active;
+ },
+ isSubmitTestPayloadDisabled() {
+ return (
+ !this.active ||
+ Boolean(this.integrationTestPayload.error) ||
+ this.integrationTestPayload.json === ''
+ );
},
},
watch: {
@@ -257,7 +273,7 @@ export default {
}
this.selectedIntegration = val.type;
this.active = val.active;
- if (val.type === typeSet.http) this.getIntegrationMapping(val.id);
+ if (val.type === typeSet.http && this.showMappingBuilder) this.getIntegrationMapping(val.id);
return this.integrationTypeSelect();
},
},
@@ -297,14 +313,8 @@ export default {
});
},
submitWithTestPayload() {
- return service
- .updateTestAlert(this.testAlertPayload)
- .then(() => {
- this.submit();
- })
- .catch(() => {
- this.$emit('test-payload-failure');
- });
+ this.$emit('set-test-alert-payload', this.testAlertPayload);
+ this.submit();
},
submit() {
// TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657
@@ -323,6 +333,7 @@ export default {
return this.$emit('update-integration', integrationPayload);
}
+ this.reset();
return this.$emit('create-new-integration', integrationPayload);
},
reset() {
@@ -539,7 +550,7 @@ export default {
</template>
</gl-form-input-group>
- <gl-button v-gl-modal.authKeyModal :disabled="!active">
+ <gl-button v-gl-modal.authKeyModal :disabled="isResetAuthKeyDisabled">
{{ $options.i18n.integrationFormSteps.step3.reset }}
</gl-button>
<gl-modal
@@ -642,7 +653,7 @@ export default {
<gl-button
v-if="!isManagingOpsgenie"
data-testid="integration-test-and-submit"
- :disabled="Boolean(integrationTestPayload.error)"
+ :disabled="isSubmitTestPayloadDisabled"
category="secondary"
variant="success"
class="gl-mx-3 js-no-auto-disable"
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue
deleted file mode 100644
index 0246315bdc5..00000000000
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_old.vue
+++ /dev/null
@@ -1,494 +0,0 @@
-<script>
-import {
- GlAlert,
- GlButton,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormInputGroup,
- GlFormTextarea,
- GlLink,
- GlModal,
- GlModalDirective,
- GlSprintf,
- GlFormSelect,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
-import { doesHashExistInUrl } from '~/lib/utils/url_utility';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
-import csrf from '~/lib/utils/csrf';
-import service from '../services';
-import {
- i18n,
- integrationTypes,
- JSON_VALIDATE_DELAY,
- targetPrometheusUrlPlaceholder,
- targetOpsgenieUrlPlaceholder,
- sectionHash,
-} from '../constants';
-import createFlash, { FLASH_TYPES } from '~/flash';
-
-export default {
- i18n,
- csrf,
- targetOpsgenieUrlPlaceholder,
- targetPrometheusUrlPlaceholder,
- components: {
- GlAlert,
- GlButton,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormInputGroup,
- GlFormSelect,
- GlFormTextarea,
- GlLink,
- GlModal,
- GlSprintf,
- ClipboardButton,
- ToggleButton,
- },
- directives: {
- 'gl-modal': GlModalDirective,
- },
- inject: ['prometheus', 'generic', 'opsgenie'],
- data() {
- return {
- loading: false,
- selectedIntegration: integrationTypes[0].value,
- options: integrationTypes,
- active: false,
- token: '',
- targetUrl: '',
- feedback: {
- variant: 'danger',
- feedbackMessage: '',
- isFeedbackDismissed: false,
- },
- testAlert: {
- json: null,
- error: null,
- },
- canSaveForm: false,
- serverError: null,
- };
- },
- computed: {
- sections() {
- return [
- {
- text: this.$options.i18n.usageSection,
- url: this.generic.alertsUsageUrl,
- },
- {
- text: this.$options.i18n.setupSection,
- url: this.generic.alertsSetupUrl,
- },
- ];
- },
- isPrometheus() {
- return this.selectedIntegration === 'PROMETHEUS';
- },
- isOpsgenie() {
- return this.selectedIntegration === 'OPSGENIE';
- },
- selectedIntegrationType() {
- switch (this.selectedIntegration) {
- case 'HTTP': {
- return {
- url: this.generic.url,
- token: this.generic.token,
- active: this.generic.active,
- resetKey: this.resetKey.bind(this),
- };
- }
- case 'PROMETHEUS': {
- return {
- url: this.prometheus.url,
- token: this.prometheus.token,
- active: this.prometheus.active,
- resetKey: this.resetKey.bind(this, 'PROMETHEUS'),
- targetUrl: this.prometheus.prometheusApiUrl,
- };
- }
- case 'OPSGENIE': {
- return {
- targetUrl: this.opsgenie.opsgenieMvcTargetUrl,
- active: this.opsgenie.active,
- };
- }
- default: {
- return {};
- }
- }
- },
- showFeedbackMsg() {
- return this.feedback.feedbackMessage && !this.isFeedbackDismissed;
- },
- showAlertSave() {
- return (
- this.feedback.feedbackMessage === this.$options.i18n.testAlertFailed &&
- !this.isFeedbackDismissed
- );
- },
- prometheusInfo() {
- return this.isPrometheus ? this.$options.i18n.prometheusInfo : '';
- },
- jsonIsValid() {
- return this.testAlert.error === null;
- },
- canTestAlert() {
- return this.active && this.testAlert.json !== null;
- },
- canSaveConfig() {
- return !this.loading && this.canSaveForm;
- },
- baseUrlPlaceholder() {
- return this.isOpsgenie
- ? this.$options.targetOpsgenieUrlPlaceholder
- : this.$options.targetPrometheusUrlPlaceholder;
- },
- },
- watch: {
- 'testAlert.json': debounce(function debouncedJsonValidate() {
- this.validateJson();
- }, JSON_VALIDATE_DELAY),
- targetUrl(oldVal, newVal) {
- if (newVal && oldVal !== this.selectedIntegrationType.targetUrl) {
- this.canSaveForm = true;
- }
- },
- },
- mounted() {
- if (this.prometheus.active || this.generic.active || !this.opsgenie.opsgenieMvcIsAvailable) {
- this.removeOpsGenieOption();
- } else if (this.opsgenie.active) {
- this.setOpsgenieAsDefault();
- }
- this.active = this.selectedIntegrationType.active;
- this.token = this.selectedIntegrationType.token ?? '';
- },
- methods: {
- createUserErrorMessage(errors = {}) {
- const error = Object.entries(errors)?.[0];
- if (error) {
- const [field, [msg]] = error;
- this.serverError = `${field} ${msg}`;
- }
- },
- setOpsgenieAsDefault() {
- this.options = this.options.map(el => {
- if (el.value !== 'OPSGENIE') {
- return { ...el, disabled: true };
- }
- return { ...el, disabled: false };
- });
- this.selectedIntegration = this.options.find(({ value }) => value === 'OPSGENIE').value;
- if (this.targetUrl === null) {
- this.targetUrl = this.selectedIntegrationType.targetUrl;
- }
- },
- removeOpsGenieOption() {
- this.options = this.options.map(el => {
- if (el.value !== 'OPSGENIE') {
- return { ...el, disabled: false };
- }
- return { ...el, disabled: true };
- });
- },
- resetFormValues() {
- this.testAlert.json = null;
- this.targetUrl = this.selectedIntegrationType.targetUrl;
- this.active = this.selectedIntegrationType.active;
- },
- dismissFeedback() {
- this.serverError = null;
- this.feedback = { ...this.feedback, feedbackMessage: null };
- this.isFeedbackDismissed = false;
- },
- resetKey(key) {
- const fn = key === 'PROMETHEUS' ? this.resetPrometheusKey() : this.resetGenericKey();
-
- return fn
- .then(({ data: { token } }) => {
- this.token = token;
- this.setFeedback({ feedbackMessage: this.$options.i18n.tokenRest, 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 });
- },
- toggleService(value) {
- this.canSaveForm = true;
- this.active = value;
- },
- toggle(value) {
- return this.isPrometheus ? this.togglePrometheusActive(value) : this.toggleActivated(value);
- },
- toggleActivated(value) {
- this.loading = true;
- const path = this.isOpsgenie ? this.opsgenie.formPath : this.generic.formPath;
- return service
- .updateGenericActive({
- endpoint: path,
- params: this.isOpsgenie
- ? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } }
- : { service: { active: value } },
- })
- .then(() => this.notifySuccessAndReload())
- .catch(({ response: { data: { errors } = {} } = {} }) => {
- this.createUserErrorMessage(errors);
- this.setFeedback({
- feedbackMessage: this.$options.i18n.errorMsg,
- variant: 'danger',
- });
- })
- .finally(() => {
- this.loading = false;
- this.canSaveForm = false;
- });
- },
- reload() {
- if (!doesHashExistInUrl(sectionHash)) {
- window.location.hash = sectionHash;
- }
- window.location.reload();
- },
- togglePrometheusActive(value) {
- this.loading = true;
- return service
- .updatePrometheusActive({
- endpoint: this.prometheus.prometheusFormPath,
- params: {
- token: this.$options.csrf.token,
- config: value,
- url: this.targetUrl,
- redirect: window.location,
- },
- })
- .then(() => this.notifySuccessAndReload())
- .catch(({ response: { data: { errors } = {} } = {} }) => {
- this.createUserErrorMessage(errors);
- this.setFeedback({
- feedbackMessage: this.$options.i18n.errorMsg,
- variant: 'danger',
- });
- })
- .finally(() => {
- this.loading = false;
- this.canSaveForm = false;
- });
- },
- notifySuccessAndReload() {
- createFlash({ message: this.$options.i18n.changesSaved, type: FLASH_TYPES.NOTICE });
- setTimeout(() => this.reload(), 1000);
- },
- setFeedback({ feedbackMessage, variant }) {
- this.feedback = { feedbackMessage, variant };
- },
- validateJson() {
- this.testAlert.error = null;
- try {
- JSON.parse(this.testAlert.json);
- } catch (e) {
- this.testAlert.error = JSON.stringify(e.message);
- }
- },
- validateTestAlert() {
- this.loading = true;
- this.dismissFeedback();
- this.validateJson();
- return service
- .updateTestAlert({
- endpoint: this.selectedIntegrationType.url,
- data: this.testAlert.json,
- token: this.selectedIntegrationType.token,
- })
- .then(() => {
- this.setFeedback({
- feedbackMessage: this.$options.i18n.testAlertSuccess,
- variant: 'success',
- });
- })
- .catch(() => {
- this.setFeedback({
- feedbackMessage: this.$options.i18n.testAlertFailed,
- variant: 'danger',
- });
- })
- .finally(() => {
- this.loading = false;
- });
- },
- onSubmit() {
- this.dismissFeedback();
- this.toggle(this.active);
- },
- onReset() {
- this.testAlert.json = null;
- this.dismissFeedback();
- this.targetUrl = this.selectedIntegrationType.targetUrl;
-
- if (this.canSaveForm) {
- this.canSaveForm = false;
- this.active = this.selectedIntegrationType.active;
- }
- },
- },
-};
-</script>
-
-<template>
- <gl-form @submit.prevent="onSubmit" @reset.prevent="onReset">
- <h5 class="gl-font-lg gl-my-5">{{ $options.i18n.integrationsLabel }}</h5>
-
- <gl-alert v-if="showFeedbackMsg" :variant="feedback.variant" @dismiss="dismissFeedback">
- {{ feedback.feedbackMessage }}
- <br />
- <i v-if="serverError">{{ __('Error message:') }} {{ serverError }}</i>
- <gl-button
- v-if="showAlertSave"
- variant="danger"
- category="primary"
- class="gl-display-block gl-mt-3"
- @click="toggle(active)"
- >
- {{ __('Save anyway') }}
- </gl-button>
- </gl-alert>
-
- <div data-testid="alert-settings-description">
- <p v-for="section in sections" :key="section.text">
- <gl-sprintf :message="section.text">
- <template #link="{ content }">
- <gl-link :href="section.url" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
-
- <gl-form-group label-for="integration-type" :label="$options.i18n.integration">
- <gl-form-select
- id="integration-type"
- v-model="selectedIntegration"
- :options="options"
- data-testid="alert-settings-select"
- @change="resetFormValues"
- />
- <span class="gl-text-gray-500">
- <gl-sprintf :message="$options.i18n.integrationsInfo">
- <template #link="{ content }">
- <gl-link
- class="gl-display-inline-block"
- href="https://gitlab.com/groups/gitlab-org/-/epics/4390"
- target="_blank"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- </span>
- </gl-form-group>
- <gl-form-group :label="$options.i18n.activeLabel" label-for="active">
- <toggle-button
- id="active"
- :disabled-input="loading"
- :is-loading="loading"
- :value="active"
- @change="toggleService"
- />
- </gl-form-group>
- <gl-form-group
- v-if="isOpsgenie || isPrometheus"
- :label="$options.i18n.apiBaseUrlLabel"
- label-for="api-url"
- >
- <gl-form-input
- id="api-url"
- v-model="targetUrl"
- type="url"
- :placeholder="baseUrlPlaceholder"
- :disabled="!active"
- />
- <span class="gl-text-gray-500">
- {{ $options.i18n.apiBaseUrlHelpText }}
- </span>
- </gl-form-group>
- <template v-if="!isOpsgenie">
- <gl-form-group :label="$options.i18n.urlLabel" label-for="url">
- <gl-form-input-group id="url" readonly :value="selectedIntegrationType.url">
- <template #append>
- <clipboard-button
- :text="selectedIntegrationType.url"
- :title="$options.i18n.copyToClipboard"
- class="gl-m-0!"
- />
- </template>
- </gl-form-input-group>
- <span class="gl-text-gray-500">
- {{ prometheusInfo }}
- </span>
- </gl-form-group>
- <gl-form-group :label="$options.i18n.tokenLabel" label-for="authorization-key">
- <gl-form-input-group id="authorization-key" class="gl-mb-2" readonly :value="token">
- <template #append>
- <clipboard-button
- :text="token"
- :title="$options.i18n.copyToClipboard"
- class="gl-m-0!"
- />
- </template>
- </gl-form-input-group>
- <gl-button v-gl-modal.tokenModal :disabled="!active" class="gl-mt-3">{{
- $options.i18n.resetKey
- }}</gl-button>
- <gl-modal
- modal-id="tokenModal"
- :title="$options.i18n.resetKey"
- :ok-title="$options.i18n.resetKey"
- ok-variant="danger"
- @ok="selectedIntegrationType.resetKey"
- >
- {{ $options.i18n.restKeyInfo }}
- </gl-modal>
- </gl-form-group>
- <gl-form-group
- :label="$options.i18n.alertJson"
- label-for="alert-json"
- :invalid-feedback="testAlert.error"
- >
- <gl-form-textarea
- id="alert-json"
- v-model.trim="testAlert.json"
- :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>
- </template>
- <div class="footer-block row-content-block gl-display-flex gl-justify-content-space-between">
- <gl-button variant="success" category="primary" :disabled="!canSaveConfig" @click="onSubmit">
- {{ __('Save changes') }}
- </gl-button>
- <gl-button category="primary" :disabled="!canSaveConfig" @click="onReset">
- {{ __('Cancel') }}
- </gl-button>
- </div>
- </gl-form>
-</template>
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index 1ffc2f80148..1ac63fd04a1 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -1,7 +1,6 @@
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql';
@@ -15,8 +14,8 @@ import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutati
import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql';
import updateCurrentIntergrationMutation from '../graphql/mutations/update_current_intergration.mutation.graphql';
import IntegrationsList from './alerts_integrations_list.vue';
-import SettingsFormOld from './alerts_settings_form_old.vue';
-import SettingsFormNew from './alerts_settings_form_new.vue';
+import AlertSettingsForm from './alerts_settings_form.vue';
+import service from '../services';
import { typeSet } from '../constants';
import {
updateStoreAfterIntegrationDelete,
@@ -37,6 +36,9 @@ export default {
'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
),
integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'),
+ alertSent: s__(
+ 'AlertsIntegrations|The test alert has been successfully sent, and should now be visible on your alerts list.',
+ ),
},
components: {
// TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657
@@ -44,10 +46,8 @@ export default {
GlLink,
GlSprintf,
IntegrationsList,
- SettingsFormOld,
- SettingsFormNew,
+ AlertSettingsForm,
},
- mixins: [glFeatureFlagsMixin()],
inject: {
generic: {
default: {},
@@ -93,6 +93,7 @@ export default {
data() {
return {
isUpdating: false,
+ testAlertPayload: null,
integrations: {},
currentIntegration: null,
};
@@ -101,20 +102,6 @@ export default {
loading() {
return this.$apollo.queries.integrations.loading;
},
- integrationsOptionsOld() {
- return [
- {
- name: s__('AlertSettings|HTTP endpoint'),
- type: s__('AlertsIntegrations|HTTP endpoint'),
- active: this.generic.active,
- },
- {
- name: s__('AlertSettings|External Prometheus'),
- type: s__('AlertsIntegrations|Prometheus'),
- active: this.prometheus.active,
- },
- ];
- },
canAddIntegration() {
return this.multiIntegrations || this.integrations?.list?.length < 2;
},
@@ -149,6 +136,19 @@ export default {
if (error) {
return createFlash({ message: error });
}
+
+ if (this.testAlertPayload) {
+ const integration =
+ httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
+
+ const payload = {
+ ...this.testAlertPayload,
+ endpoint: integration.url,
+ token: integration.token,
+ };
+ return this.validateAlertPayload(payload);
+ }
+
return createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
@@ -179,6 +179,13 @@ export default {
if (error) {
return createFlash({ message: error });
}
+
+ if (this.testAlertPayload) {
+ return this.validateAlertPayload();
+ }
+
+ this.clearCurrentIntegration();
+
return createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
@@ -189,6 +196,7 @@ export default {
})
.finally(() => {
this.isUpdating = false;
+ this.testAlertPayload = null;
});
},
resetToken({ type, variables }) {
@@ -212,7 +220,13 @@ export default {
const integration =
httpIntegrationResetToken?.integration ||
prometheusIntegrationResetToken?.integration;
- this.currentIntegration = integration;
+
+ this.$apollo.mutate({
+ mutation: updateCurrentIntergrationMutation,
+ variables: {
+ ...integration,
+ },
+ });
return createFlash({
message: this.$options.i18n.changesSaved,
@@ -280,8 +294,21 @@ export default {
variables: {},
});
},
- testPayloadFailure() {
- createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
+ setTestAlertPayload(payload) {
+ this.testAlertPayload = payload;
+ },
+ validateAlertPayload(payload) {
+ return service
+ .updateTestAlert(payload ?? this.testAlertPayload)
+ .then(() => {
+ return createFlash({
+ message: this.$options.i18n.alertSent,
+ type: FLASH_TYPES.SUCCESS,
+ });
+ })
+ .catch(() => {
+ createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
+ });
},
},
};
@@ -310,13 +337,12 @@ export default {
</gl-alert>
<integrations-list
v-else
- :integrations="glFeatures.httpIntegrationsList ? integrations.list : integrationsOptionsOld"
+ :integrations="integrations.list"
:loading="loading"
@edit-integration="editIntegration"
@delete-integration="deleteIntegration"
/>
- <settings-form-new
- v-if="glFeatures.httpIntegrationsList"
+ <alert-settings-form
:loading="isUpdating"
:can-add-integration="canAddIntegration"
:can-manage-opsgenie="canManageOpsgenie"
@@ -324,8 +350,7 @@ export default {
@update-integration="updateIntegration"
@reset-token="resetToken"
@clear-current-integration="clearCurrentIntegration"
- @test-payload-failure="testPayloadFailure"
+ @set-test-alert-payload="setTestAlertPayload"
/>
- <settings-form-old v-else />
</div>
</template>
diff --git a/app/assets/javascripts/alerts_settings/services/index.js b/app/assets/javascripts/alerts_settings/services/index.js
index 1835d6b46aa..e45ea772ddd 100644
--- a/app/assets/javascripts/alerts_settings/services/index.js
+++ b/app/assets/javascripts/alerts_settings/services/index.js
@@ -2,30 +2,9 @@
import axios from '~/lib/utils/axios_utils';
export default {
- // TODO: All this code save updateTestAlert will be deleted as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/255501
- updateGenericKey({ endpoint, params }) {
- return axios.put(endpoint, params);
- },
- updatePrometheusKey({ endpoint }) {
- return axios.post(endpoint);
- },
updateGenericActive({ endpoint, params }) {
return axios.put(endpoint, params);
},
- updatePrometheusActive({ endpoint, params: { token, config, url, redirect } }) {
- const data = new FormData();
- data.set('_method', 'put');
- data.set('authenticity_token', token);
- data.set('service[manual_configuration]', config);
- data.set('service[api_url]', url);
- data.set('redirect_to', redirect);
-
- return axios.post(endpoint, data, {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- });
- },
updateTestAlert({ endpoint, data, token }) {
return axios.post(endpoint, data, {
headers: {
diff --git a/app/assets/javascripts/behaviors/select2.js b/app/assets/javascripts/behaviors/select2.js
index 37b75bb5e56..1f222d8c1f6 100644
--- a/app/assets/javascripts/behaviors/select2.js
+++ b/app/assets/javascripts/behaviors/select2.js
@@ -1,22 +1,29 @@
import $ from 'jquery';
+import { loadCSSFile } from '../lib/utils/css_utils';
export default () => {
- if ($('select.select2').length) {
+ const $select2Elements = $('select.select2');
+ if ($select2Elements.length) {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- $('select.select2').select2({
- width: 'resolve',
- minimumResultsForSearch: 10,
- dropdownAutoWidth: true,
- });
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $select2Elements.select2({
+ width: 'resolve',
+ minimumResultsForSearch: 10,
+ dropdownAutoWidth: true,
+ });
- // Close select2 on escape
- $('.js-select2').on('select2-close', () => {
- setTimeout(() => {
- $('.select2-container-active').removeClass('select2-container-active');
- $(':focus').blur();
- }, 1);
- });
+ // Close select2 on escape
+ $('.js-select2').on('select2-close', () => {
+ requestAnimationFrame(() => {
+ $('.select2-container-active').removeClass('select2-container-active');
+ $(':focus').blur();
+ });
+ });
+ })
+ .catch(() => {});
})
.catch(() => {});
}
diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js
index bd39aa2e16f..2532aeea989 100644
--- a/app/assets/javascripts/blob/file_template_selector.js
+++ b/app/assets/javascripts/blob/file_template_selector.js
@@ -12,7 +12,10 @@ export default class FileTemplateSelector {
this.$dropdown = $(cfg.dropdown);
this.$wrapper = $(cfg.wrapper);
- this.$loadingIcon = this.$wrapper.find('.fa-chevron-down');
+ this.$dropdownIcon = this.$wrapper.find('.dropdown-menu-toggle-icon');
+ this.$loadingIcon = $(
+ '<div class="gl-spinner gl-spinner-orange gl-spinner-sm gl-absolute gl-top-3 gl-right-3 gl-display-none"></div>',
+ ).insertAfter(this.$dropdownIcon);
this.$dropdownToggleText = this.$wrapper.find('.dropdown-toggle-text');
this.initDropdown();
@@ -45,15 +48,13 @@ export default class FileTemplateSelector {
}
renderLoading() {
- this.$loadingIcon
- .addClass('gl-spinner gl-spinner-orange gl-spinner-sm')
- .removeClass('fa-chevron-down');
+ this.$loadingIcon.removeClass('gl-display-none');
+ this.$dropdownIcon.addClass('gl-display-none');
}
renderLoaded() {
- this.$loadingIcon
- .addClass('fa-chevron-down')
- .removeClass('gl-spinner gl-spinner-orange gl-spinner-sm');
+ this.$loadingIcon.addClass('gl-display-none');
+ this.$dropdownIcon.removeClass('gl-display-none');
}
reportSelection(options) {
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index 257458138dc..ae9bb3455f0 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -10,7 +10,10 @@ export default class TemplateSelector {
this.dropdown = dropdown;
this.$dropdownContainer = wrapper;
this.$filenameInput = $input || $('#file_name');
- this.$dropdownIcon = $('.fa-chevron-down', dropdown);
+ this.$dropdownIcon = $('.dropdown-menu-toggle-icon', dropdown);
+ this.$loadingIcon = $(
+ '<div class="gl-spinner gl-spinner-orange gl-spinner-sm gl-absolute gl-top-3 gl-right-3 gl-display-none"></div>',
+ ).insertAfter(this.$dropdownIcon);
this.initDropdown(dropdown, data);
this.listenForFilenameInput();
@@ -92,10 +95,12 @@ export default class TemplateSelector {
}
startLoadingSpinner() {
- this.$dropdownIcon.addClass('spinner').removeClass('fa-chevron-down');
+ this.$loadingIcon.removeClass('gl-display-none');
+ this.$dropdownIcon.addClass('gl-display-none');
}
stopLoadingSpinner() {
- this.$dropdownIcon.addClass('fa-chevron-down').removeClass('spinner');
+ this.$loadingIcon.addClass('gl-display-none');
+ this.$dropdownIcon.removeClass('gl-display-none');
}
}
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 6b7b0c2e28d..0142c95e773 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,5 +1,4 @@
import { sortBy } from 'lodash';
-import ListIssue from 'ee_else_ce/boards/models/issue';
import { ListType } from './constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import boardsStore from '~/boards/stores/boards_store';
@@ -21,11 +20,11 @@ export function formatBoardLists(lists) {
}
export function formatIssue(issue) {
- return new ListIssue({
+ return {
...issue,
labels: issue.labels?.nodes || [],
assignees: issue.assignees?.nodes || [],
- });
+ };
}
export function formatListIssues(listIssues) {
@@ -44,12 +43,12 @@ export function formatListIssues(listIssues) {
[list.id]: sortedIssues.map(i => {
const id = getIdFromGraphQLId(i.id);
- const listIssue = new ListIssue({
+ const listIssue = {
...i,
id,
labels: i.labels?.nodes || [],
assignees: i.assignees?.nodes || [],
- });
+ };
issues[id] = listIssue;
@@ -83,21 +82,30 @@ export function fullLabelId(label) {
}
export function moveIssueListHelper(issue, fromList, toList) {
- if (toList.type === ListType.label) {
- issue.addLabel(toList.label);
+ const updatedIssue = issue;
+ if (
+ toList.type === ListType.label &&
+ !updatedIssue.labels.find(label => label.id === toList.label.id)
+ ) {
+ updatedIssue.labels.push(toList.label);
}
- if (fromList && fromList.type === ListType.label) {
- issue.removeLabel(fromList.label);
+ if (fromList?.label && fromList.type === ListType.label) {
+ updatedIssue.labels = updatedIssue.labels.filter(label => fromList.label.id !== label.id);
}
- if (toList.type === ListType.assignee) {
- issue.addAssignee(toList.assignee);
+ if (
+ toList.type === ListType.assignee &&
+ !updatedIssue.assignees.find(assignee => assignee.id === toList.assignee.id)
+ ) {
+ updatedIssue.assignees.push(toList.assignee);
}
- if (fromList && fromList.type === ListType.assignee) {
- issue.removeAssignee(fromList.assignee);
+ if (fromList?.assignee && fromList.type === ListType.assignee) {
+ updatedIssue.assignees = updatedIssue.assignees.filter(
+ assignee => assignee.id !== fromList.assignee.id,
+ );
}
- return issue;
+ return updatedIssue;
}
export default {
diff --git a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
index c81f171af2b..231cde0d24f 100644
--- a/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
+++ b/app/assets/javascripts/boards/components/board_assignee_dropdown.vue
@@ -1,11 +1,13 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import { cloneDeep } from 'lodash';
import {
GlDropdownItem,
GlDropdownDivider,
GlAvatarLabeled,
GlAvatarLink,
GlSearchBoxByType,
+ GlLoadingIcon,
} from '@gitlab/ui';
import { __, n__ } from '~/locale';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
@@ -32,12 +34,13 @@ export default {
GlAvatarLabeled,
GlAvatarLink,
GlSearchBoxByType,
+ GlLoadingIcon,
},
data() {
return {
search: '',
participants: [],
- selected: this.$store.getters.activeIssue.assignees,
+ selected: [],
};
},
apollo: {
@@ -89,9 +92,20 @@ export default {
isSearchEmpty() {
return this.search === '';
},
+ currentUser() {
+ return gon?.current_username;
+ },
+ },
+ created() {
+ this.selected = cloneDeep(this.activeIssue.assignees);
},
methods: {
...mapActions(['setAssignees']),
+ async assignSelf() {
+ const [currentUserObject] = await this.setAssignees(this.currentUser);
+
+ this.selectAssignee(currentUserObject);
+ },
clearSelected() {
this.selected = [];
},
@@ -119,7 +133,7 @@ export default {
<template>
<board-editable-item :title="assigneeText" @close="saveAssignees">
<template #collapsed>
- <issuable-assignees :users="activeIssue.assignees" />
+ <issuable-assignees :users="selected" @assign-self="assignSelf" />
</template>
<template #default>
@@ -132,45 +146,48 @@ export default {
<gl-search-box-by-type v-model.trim="search" />
</template>
<template #items>
- <gl-dropdown-item
- :is-checked="selectedIsEmpty"
- data-testid="unassign"
- class="mt-2"
- @click="selectAssignee()"
- >{{ $options.i18n.unassigned }}</gl-dropdown-item
- >
- <gl-dropdown-divider data-testid="unassign-divider" />
- <gl-dropdown-item
- v-for="item in selected"
- :key="item.id"
- :is-checked="isChecked(item.username)"
- @click="unselect(item.username)"
- >
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="item.name"
- :sub-label="item.username"
- :src="item.avatarUrl || item.avatar"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
- <gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" />
- <gl-dropdown-item
- v-for="unselectedUser in unSelectedFiltered"
- :key="unselectedUser.id"
- :data-testid="`item_${unselectedUser.name}`"
- @click="selectAssignee(unselectedUser)"
- >
- <gl-avatar-link>
- <gl-avatar-labeled
- :size="32"
- :label="unselectedUser.name"
- :sub-label="unselectedUser.username"
- :src="unselectedUser.avatarUrl || unselectedUser.avatar"
- />
- </gl-avatar-link>
- </gl-dropdown-item>
+ <gl-loading-icon v-if="$apollo.queries.participants.loading" size="lg" />
+ <template v-else>
+ <gl-dropdown-item
+ :is-checked="selectedIsEmpty"
+ data-testid="unassign"
+ class="mt-2"
+ @click="selectAssignee()"
+ >{{ $options.i18n.unassigned }}</gl-dropdown-item
+ >
+ <gl-dropdown-divider data-testid="unassign-divider" />
+ <gl-dropdown-item
+ v-for="item in selected"
+ :key="item.id"
+ :is-checked="isChecked(item.username)"
+ @click="unselect(item.username)"
+ >
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="item.name"
+ :sub-label="item.username"
+ :src="item.avatarUrl || item.avatar"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ <gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" />
+ <gl-dropdown-item
+ v-for="unselectedUser in unSelectedFiltered"
+ :key="unselectedUser.id"
+ :data-testid="`item_${unselectedUser.name}`"
+ @click="selectAssignee(unselectedUser)"
+ >
+ <gl-avatar-link>
+ <gl-avatar-labeled
+ :size="32"
+ :label="unselectedUser.name"
+ :sub-label="unselectedUser.username"
+ :src="unselectedUser.avatarUrl || unselectedUser.avatar"
+ />
+ </gl-avatar-link>
+ </gl-dropdown-item>
+ </template>
</template>
</multi-select-dropdown>
</template>
diff --git a/app/assets/javascripts/boards/components/board_column_new.vue b/app/assets/javascripts/boards/components/board_column_new.vue
index 8a59355eb83..7162423846b 100644
--- a/app/assets/javascripts/boards/components/board_column_new.vue
+++ b/app/assets/javascripts/boards/components/board_column_new.vue
@@ -85,6 +85,7 @@ export default {
:disabled="disabled"
:issues="listIssues"
:list="list"
+ :can-admin-list="canAdminList"
/>
<!-- Will be only available in EE -->
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 53989e2d9de..1f87b563e73 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,7 +6,6 @@ import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
getBoardSortableDefaultOptions,
@@ -25,7 +24,6 @@ export default {
boardNewIssue,
GlLoadingIcon,
},
- mixins: [glFeatureFlagMixin()],
props: {
disabled: {
type: Boolean,
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index d85ba2038a7..722bd20f227 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -190,7 +190,8 @@ export default {
:title="chevronTooltip"
:icon="chevronIcon"
class="board-title-caret no-drag gl-cursor-pointer"
- variant="link"
+ category="tertiary"
+ size="small"
@click="toggleExpanded"
/>
<!-- The following is only true in EE and if it is a milestone -->
diff --git a/app/assets/javascripts/boards/components/board_list_header_new.vue b/app/assets/javascripts/boards/components/board_list_header_new.vue
index 99347a4cd4d..447fef4b49c 100644
--- a/app/assets/javascripts/boards/components/board_list_header_new.vue
+++ b/app/assets/javascripts/boards/components/board_list_header_new.vue
@@ -198,7 +198,8 @@ export default {
:title="chevronTooltip"
:icon="chevronIcon"
class="board-title-caret no-drag gl-cursor-pointer"
- variant="link"
+ category="tertiary"
+ size="small"
@click="toggleExpanded"
/>
<!-- EE start -->
diff --git a/app/assets/javascripts/boards/components/board_list_new.vue b/app/assets/javascripts/boards/components/board_list_new.vue
index 396aedcc557..291abc9849b 100644
--- a/app/assets/javascripts/boards/components/board_list_new.vue
+++ b/app/assets/javascripts/boards/components/board_list_new.vue
@@ -1,12 +1,14 @@
<script>
+import Draggable from 'vuedraggable';
import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
+import defaultSortableConfig from '~/sortable/sortable_config';
+import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options';
import BoardNewIssue from './board_new_issue_new.vue';
import BoardCard from './board_card.vue';
import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'BoardList',
@@ -15,7 +17,6 @@ export default {
BoardNewIssue,
GlLoadingIcon,
},
- mixins: [glFeatureFlagMixin()],
props: {
disabled: {
type: Boolean,
@@ -29,6 +30,11 @@ export default {
type: Array,
required: true,
},
+ canAdminList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -55,12 +61,32 @@ export default {
loading() {
return this.listsFlags[this.list.id]?.isLoading;
},
+ listRef() {
+ // When list is draggable, the reference to the list needs to be accessed differently
+ return this.canAdminList ? this.$refs.list.$el : this.$refs.list;
+ },
+ treeRootWrapper() {
+ return this.canAdminList ? Draggable : 'ul';
+ },
+ treeRootOptions() {
+ const options = {
+ ...defaultSortableConfig,
+ fallbackOnBody: false,
+ group: 'boards-list',
+ tag: 'ul',
+ 'ghost-class': 'board-card-drag-active',
+ 'data-list-id': this.list.id,
+ value: this.issues,
+ };
+
+ return this.canAdminList ? options : {};
+ },
},
watch: {
filters: {
handler() {
this.list.loadingMore = false;
- this.$refs.list.scrollTop = 0;
+ this.listRef.scrollTop = 0;
},
deep: true,
},
@@ -76,26 +102,26 @@ export default {
},
mounted() {
// Scroll event on list to load more
- this.$refs.list.addEventListener('scroll', this.onScroll);
+ this.listRef.addEventListener('scroll', this.onScroll);
},
beforeDestroy() {
eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
- this.$refs.list.removeEventListener('scroll', this.onScroll);
+ this.listRef.removeEventListener('scroll', this.onScroll);
},
methods: {
- ...mapActions(['fetchIssuesForList']),
+ ...mapActions(['fetchIssuesForList', 'moveIssue']),
listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
+ return this.listRef.getBoundingClientRect().height;
},
scrollHeight() {
- return this.$refs.list.scrollHeight;
+ return this.listRef.scrollHeight;
},
scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
+ return this.listRef.scrollTop + this.listHeight();
},
scrollToTop() {
- this.$refs.list.scrollTop = 0;
+ this.listRef.scrollTop = 0;
},
loadNextPage() {
const loadingDone = () => {
@@ -120,6 +146,52 @@ export default {
}
});
},
+ handleDragOnStart() {
+ sortableStart();
+ },
+ handleDragOnEnd(params) {
+ sortableEnd();
+ const { newIndex, oldIndex, from, to, item } = params;
+ const { issueId, issueIid, issuePath } = item.dataset;
+ const { children } = to;
+ let moveBeforeId;
+ let moveAfterId;
+
+ const getIssueId = el => Number(el.dataset.issueId);
+
+ // If issue is being moved within the same list
+ if (from === to) {
+ if (newIndex > oldIndex && children.length > 1) {
+ // If issue is being moved down we look for the issue that ends up before
+ moveBeforeId = getIssueId(children[newIndex]);
+ } else if (newIndex < oldIndex && children.length > 1) {
+ // If issue is being moved up we look for the issue that ends up after
+ moveAfterId = getIssueId(children[newIndex]);
+ } else {
+ // If issue remains in the same list at the same position we do nothing
+ return;
+ }
+ } else {
+ // We look for the issue that ends up before the moved issue if it exists
+ if (children[newIndex - 1]) {
+ moveBeforeId = getIssueId(children[newIndex - 1]);
+ }
+ // We look for the issue that ends up after the moved issue if it exists
+ if (children[newIndex]) {
+ moveAfterId = getIssueId(children[newIndex]);
+ }
+ }
+
+ this.moveIssue({
+ issueId,
+ issueIid,
+ issuePath,
+ fromListId: from.dataset.listId,
+ toListId: to.dataset.listId,
+ moveBeforeId,
+ moveAfterId,
+ });
+ },
},
};
</script>
@@ -139,13 +211,18 @@ export default {
<gl-loading-icon />
</div>
<board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" />
- <ul
+ <component
+ :is="treeRootWrapper"
v-show="!loading"
ref="list"
+ v-bind="treeRootOptions"
:data-board="list.id"
:data-board-type="list.type"
:class="{ 'bg-danger-100': issuesSizeExceedsMax }"
class="board-list gl-w-full gl-h-full gl-list-style-none gl-mb-0 gl-p-2 js-board-list"
+ data-testid="tree-root-wrapper"
+ @start="handleDragOnStart"
+ @end="handleDragOnEnd"
>
<board-card
v-for="(issue, index) in issues"
@@ -161,6 +238,6 @@ export default {
<span v-if="issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
<span v-else>{{ paginatedIssueText }}</span>
</li>
- </ul>
+ </component>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index 45ce1e51489..67e8a32dbe2 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -158,9 +158,13 @@ export default {
class="confidential-icon gl-mr-2"
:aria-label="__('Confidential')"
/>
- <a :href="issue.path" :title="issue.title" class="js-no-trigger" @mousemove.stop>{{
- issue.title
- }}</a>
+ <a
+ :href="issue.path || issue.webUrl || ''"
+ :title="issue.title"
+ class="js-no-trigger"
+ @mousemove.stop
+ >{{ issue.title }}</a
+ >
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
@@ -196,7 +200,11 @@ export default {
#{{ issue.iid }}
</span>
<span class="board-info-items gl-mt-3 gl-display-inline-block">
- <issue-due-date v-if="issue.dueDate" :date="issue.dueDate" :closed="issue.closed" />
+ <issue-due-date
+ v-if="issue.dueDate"
+ :date="issue.dueDate"
+ :closed="issue.closed || Boolean(issue.closedAt)"
+ />
<issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
<issue-card-weight
v-if="validIssueWeight"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
index 5fb7a9b210c..ce267be6d45 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
@@ -50,6 +50,13 @@ export default {
}
window.removeEventListener('click', this.collapseWhenOffClick);
},
+ toggle({ emitEvent = true } = {}) {
+ if (this.edit) {
+ this.collapse({ emitEvent });
+ } else {
+ this.expand();
+ }
+ },
},
};
</script>
@@ -64,18 +71,18 @@ export default {
<gl-button
v-if="canUpdate"
variant="link"
- class="gl-text-gray-900!"
+ class="gl-text-gray-900! js-sidebar-dropdown-toggle"
data-testid="edit-button"
- @click="expand()"
+ @click="toggle"
>
{{ __('Edit') }}
</gl-button>
</div>
- <div v-show="!edit" class="gl-text-gray-400" data-testid="collapsed-content">
+ <div v-show="!edit" class="gl-text-gray-500" data-testid="collapsed-content">
<slot name="collapsed">{{ __('None') }}</slot>
</div>
<div v-show="edit" data-testid="expanded-content">
- <slot></slot>
+ <slot :edit="edit"></slot>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
index 6935ead2706..904ceaed1b3 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
@@ -79,7 +79,7 @@ export default {
<span class="gl-mx-2">-</span>
<gl-button
variant="link"
- class="gl-text-gray-400!"
+ class="gl-text-gray-500!"
data-testid="reset-button"
:disabled="loading"
@click="setDueDate(null)"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index 9d537a4ef2c..6a407bd6ba6 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -92,7 +92,7 @@ export default {
@close="removeLabel(label.id)"
/>
</template>
- <template>
+ <template #default="{ edit }">
<labels-select
ref="labelsSelect"
:allow-label-edit="false"
@@ -105,6 +105,7 @@ export default {
:labels-filter-base-path="labelsFilterBasePath"
:labels-list-title="__('Select label')"
:dropdown-button-text="__('Choose labels')"
+ :is-editing="edit"
variant="embedded"
class="gl-display-block labels gl-w-full"
@updateSelectedLabels="setLabels"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
new file mode 100644
index 00000000000..f7b7fd3f61f
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_milestone_select.vue
@@ -0,0 +1,161 @@
+<script>
+import { mapGetters, mapActions } from 'vuex';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ GlDropdownDivider,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { fetchPolicies } from '~/lib/graphql';
+import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
+import groupMilestones from '../../queries/group_milestones.query.graphql';
+import createFlash from '~/flash';
+import { __, s__ } from '~/locale';
+
+export default {
+ components: {
+ BoardEditableItem,
+ GlDropdown,
+ GlLoadingIcon,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ GlDropdownDivider,
+ },
+ data() {
+ return {
+ milestones: [],
+ searchTitle: '',
+ loading: false,
+ edit: false,
+ };
+ },
+ apollo: {
+ milestones: {
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ query: groupMilestones,
+ debounce: 250,
+ skip() {
+ return !this.edit;
+ },
+ variables() {
+ return {
+ fullPath: this.groupFullPath,
+ searchTitle: this.searchTitle,
+ state: 'active',
+ includeDescendants: true,
+ };
+ },
+ update(data) {
+ const edges = data?.group?.milestones?.edges ?? [];
+ return edges.map(item => item.node);
+ },
+ error() {
+ createFlash({ message: this.$options.i18n.fetchMilestonesError });
+ },
+ },
+ },
+ computed: {
+ ...mapGetters({ issue: 'activeIssue' }),
+ hasMilestone() {
+ return this.issue.milestone !== null;
+ },
+ groupFullPath() {
+ const { referencePath = '' } = this.issue;
+ return referencePath.slice(0, referencePath.indexOf('/'));
+ },
+ projectPath() {
+ const { referencePath = '' } = this.issue;
+ return referencePath.slice(0, referencePath.indexOf('#'));
+ },
+ dropdownText() {
+ return this.issue.milestone?.title ?? this.$options.i18n.noMilestone;
+ },
+ },
+ mounted() {
+ this.$root.$on('bv::dropdown::hide', () => {
+ this.$refs.sidebarItem.collapse();
+ });
+ },
+ methods: {
+ ...mapActions(['setActiveIssueMilestone']),
+ handleOpen() {
+ this.edit = true;
+ this.$refs.dropdown.show();
+ },
+ async setMilestone(milestoneId) {
+ this.loading = true;
+ this.searchTitle = '';
+ this.$refs.sidebarItem.collapse();
+
+ try {
+ const input = { milestoneId, projectPath: this.projectPath };
+ await this.setActiveIssueMilestone(input);
+ } catch (e) {
+ createFlash({ message: this.$options.i18n.updateMilestoneError });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ i18n: {
+ milestone: __('Milestone'),
+ noMilestone: __('No milestone'),
+ assignMilestone: __('Assign milestone'),
+ noMilestonesFound: s__('Milestones|No milestones found'),
+ fetchMilestonesError: __('There was a problem fetching milestones.'),
+ updateMilestoneError: __('An error occurred while updating the milestone.'),
+ },
+};
+</script>
+
+<template>
+ <board-editable-item
+ ref="sidebarItem"
+ :title="$options.i18n.milestone"
+ :loading="loading"
+ @open="handleOpen()"
+ @close="edit = false"
+ >
+ <template v-if="hasMilestone" #collapsed>
+ <strong class="gl-text-gray-900">{{ issue.milestone.title }}</strong>
+ </template>
+ <template>
+ <gl-dropdown
+ ref="dropdown"
+ :text="dropdownText"
+ :header-text="$options.i18n.assignMilestone"
+ block
+ >
+ <gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
+ <gl-dropdown-item
+ data-testid="no-milestone-item"
+ :is-check-item="true"
+ :is-checked="!issue.milestone"
+ @click="setMilestone(null)"
+ >
+ {{ $options.i18n.noMilestone }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-loading-icon v-if="$apollo.loading" class="gl-py-4" />
+ <template v-else-if="milestones.length > 0">
+ <gl-dropdown-item
+ v-for="milestone in milestones"
+ :key="milestone.id"
+ :is-check-item="true"
+ :is-checked="issue.milestone && milestone.id === issue.milestone.id"
+ data-testid="milestone-item"
+ @click="setMilestone(milestone.id)"
+ >
+ {{ milestone.title }}
+ </gl-dropdown-item>
+ </template>
+ <gl-dropdown-text v-else data-testid="no-milestones-found">
+ {{ $options.i18n.noMilestonesFound }}
+ </gl-dropdown-text>
+ </gl-dropdown>
+ </template>
+ </board-editable-item>
+</template>
diff --git a/app/assets/javascripts/boards/queries/group_milestones.query.graphql b/app/assets/javascripts/boards/queries/group_milestones.query.graphql
new file mode 100644
index 00000000000..f2ab12ef4a7
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/group_milestones.query.graphql
@@ -0,0 +1,17 @@
+query groupMilestones(
+ $fullPath: ID!
+ $state: MilestoneStateEnum
+ $includeDescendants: Boolean
+ $searchTitle: String
+) {
+ group(fullPath: $fullPath) {
+ milestones(state: $state, includeDescendants: $includeDescendants, searchTitle: $searchTitle) {
+ edges {
+ node {
+ id
+ title
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/issue.fragment.graphql b/app/assets/javascripts/boards/queries/issue.fragment.graphql
index 4b429f875a6..1395bef39ed 100644
--- a/app/assets/javascripts/boards/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/boards/queries/issue.fragment.graphql
@@ -11,6 +11,10 @@ fragment IssueNode on Issue {
webUrl
subscribed
relativePosition
+ milestone {
+ id
+ title
+ }
assignees {
nodes {
...User
diff --git a/app/assets/javascripts/boards/queries/issue_set_milestone.mutation.graphql b/app/assets/javascripts/boards/queries/issue_set_milestone.mutation.graphql
new file mode 100644
index 00000000000..5dc78a03a06
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/issue_set_milestone.mutation.graphql
@@ -0,0 +1,12 @@
+mutation issueSetMilestone($input: UpdateIssueInput!) {
+ updateIssue(input: $input) {
+ issue {
+ milestone {
+ id
+ title
+ description
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index dd950a45076..00c64bff74e 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -25,6 +25,7 @@ import issueCreateMutation from '../queries/issue_create.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
import issueSetSubscriptionMutation from '../graphql/mutations/issue_set_subscription.mutation.graphql';
+import issueSetMilestone from '../queries/issue_set_milestone.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -325,14 +326,42 @@ export default {
},
})
.then(({ data }) => {
+ const { nodes } = data.issueSetAssignees?.issue?.assignees || [];
+
commit('UPDATE_ISSUE_BY_ID', {
issueId: getters.activeIssue.id,
prop: 'assignees',
- value: data.issueSetAssignees.issue.assignees.nodes,
+ value: nodes,
});
+
+ return nodes;
});
},
+ setActiveIssueMilestone: async ({ commit, getters }, input) => {
+ const { activeIssue } = getters;
+ const { data } = await gqlClient.mutate({
+ mutation: issueSetMilestone,
+ variables: {
+ input: {
+ iid: String(activeIssue.iid),
+ milestoneId: getIdFromGraphQLId(input.milestoneId),
+ projectPath: input.projectPath,
+ },
+ },
+ });
+
+ if (data.updateIssue.errors?.length > 0) {
+ throw new Error(data.updateIssue.errors);
+ }
+
+ commit(types.UPDATE_ISSUE_BY_ID, {
+ issueId: activeIssue.id,
+ prop: 'milestone',
+ value: data.updateIssue.issue.milestone,
+ });
+ },
+
createNewIssue: ({ commit, state }, issueInput) => {
const input = issueInput;
const { boardType, endpoints } = state;
diff --git a/app/assets/javascripts/clone_panel.js b/app/assets/javascripts/clone_panel.js
new file mode 100644
index 00000000000..362e6c5c5ce
--- /dev/null
+++ b/app/assets/javascripts/clone_panel.js
@@ -0,0 +1,42 @@
+import $ from 'jquery';
+
+export default function initClonePanel() {
+ const $cloneOptions = $('ul.clone-options-dropdown');
+ if ($cloneOptions.length) {
+ const $cloneUrlField = $('#clone_url');
+ const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
+ const mobileCloneField = document.querySelector(
+ '.js-mobile-git-clone .js-clone-dropdown-label',
+ );
+
+ const selectedCloneOption = $cloneBtnLabel.text().trim();
+ if (selectedCloneOption.length > 0) {
+ $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
+ }
+
+ $('a', $cloneOptions).on('click', e => {
+ e.preventDefault();
+ const $this = $(e.currentTarget);
+ const url = $this.attr('href');
+ const cloneType = $this.data('cloneType');
+
+ $('.is-active', $cloneOptions).removeClass('is-active');
+ $(`a[data-clone-type="${cloneType}"]`).each(function switchProtocol() {
+ const $el = $(this);
+ const activeText = $el.find('.dropdown-menu-inner-title').text();
+ const $container = $el.closest('.js-git-clone-holder, .js-mobile-git-clone');
+ const $label = $container.find('.js-clone-dropdown-label');
+
+ $el.toggleClass('is-active');
+ $label.text(activeText);
+ });
+
+ if (mobileCloneField) {
+ mobileCloneField.dataset.clipboardText = url;
+ } else {
+ $cloneUrlField.val(url);
+ }
+ $('.js-git-empty .js-clone').text(url);
+ });
+ }
+}
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index a75646db162..ba005e98d53 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -52,6 +52,7 @@ export default class Clusters {
clusterStatus,
clusterStatusReason,
helpPath,
+ helmHelpPath,
ingressHelpPath,
ingressDnsHelpPath,
ingressModSecurityHelpPath,
@@ -68,8 +69,9 @@ export default class Clusters {
this.clusterBannerDismissedKey = `cluster_${this.clusterId}_banner_dismissed`;
this.store = new ClustersStore();
- this.store.setHelpPaths(
+ this.store.setHelpPaths({
helpPath,
+ helmHelpPath,
ingressHelpPath,
ingressDnsHelpPath,
ingressModSecurityHelpPath,
@@ -78,7 +80,7 @@ export default class Clusters {
deployBoardsHelpPath,
cloudRunHelpPath,
ciliumHelpPath,
- );
+ });
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
@@ -162,6 +164,7 @@ export default class Clusters {
type,
applications: this.state.applications,
helpPath: this.state.helpPath,
+ helmHelpPath: this.state.helmHelpPath,
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
ingressDnsHelpPath: this.state.ingressDnsHelpPath,
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 271d862afab..412082b648f 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,6 +1,7 @@
<script>
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';
@@ -46,6 +47,11 @@ export default {
required: false,
default: '',
},
+ helmHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
ingressHelpPath: {
type: String,
required: false,
@@ -150,6 +156,7 @@ export default {
},
logos: {
gitlabLogo,
+ helmLogo,
jupyterhubLogo,
kubernetesLogo,
certManagerLogo,
@@ -173,6 +180,35 @@ export default {
<div class="cluster-application-list gl-mt-3">
<application-row
+ v-if="applications.helm.installed || applications.helm.uninstalling"
+ 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"
+ title-link="https://v2.helm.sh/"
+ >
+ <template #description>
+ <p>
+ {{
+ s__(`ClusterIntegration|Can be safely removed. Prior to GitLab
+ 13.2, GitLab used a remote Tiller server to manage the
+ applications. GitLab no longer uses this server.
+ Uninstalling this server will not affect your other
+ applications. This row will disappear afterwards.`)
+ }}
+ <gl-link :href="helmHelpPath">{{ __('More information') }}</gl-link>
+ </p>
+ </template>
+ </application-row>
+ <application-row
:id="ingressId"
:logo-url="$options.logos.kubernetesLogo"
:title="applications.ingress.title"
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
index cb415d902e8..d80bd6f5b42 100644
--- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue
+++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
@@ -7,6 +7,7 @@ import {
GlSearchBoxByType,
GlSprintf,
GlButton,
+ GlAlert,
} from '@gitlab/ui';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { __, s__ } from '~/locale';
@@ -25,6 +26,7 @@ export default {
GlDropdownItem,
GlSearchBoxByType,
GlSprintf,
+ GlAlert,
},
props: {
knative: {
@@ -106,12 +108,13 @@ export default {
<template>
<div class="row">
- <div
+ <gl-alert
v-if="knative.updateFailed"
- class="bs-callout bs-callout-danger cluster-application-banner col-12 mt-2 mb-2 js-cluster-knative-domain-name-failure-message"
+ class="gl-mb-5 col-12 js-cluster-knative-domain-name-failure-message"
+ variant="danger"
>
{{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
- </div>
+ </gl-alert>
<div
:class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
index 477dd13db4f..2a197e40b60 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
@@ -16,7 +16,7 @@ import {
const CUSTOM_APP_WARNING_TEXT = {
[HELM]: sprintf(
s__(
- 'ClusterIntegration|The associated Tiller pod, the %{gitlabManagedAppsNamespace} namespace, and all of its resources will be deleted and cannot be restored.',
+ 'ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored. Your other applications will remain unaffected.',
),
{
gitlabManagedAppsNamespace: '<code>gitlab-managed-apps</code>',
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
index 683b0e18534..1dd815ae44d 100644
--- a/app/assets/javascripts/clusters/services/application_state_machine.js
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -193,6 +193,12 @@ const applicationStateMachine = {
uninstallSuccessful: true,
},
},
+ [NOT_INSTALLABLE]: {
+ target: NOT_INSTALLABLE,
+ effects: {
+ uninstallSuccessful: true,
+ },
+ },
[UNINSTALL_ERRORED]: {
target: INSTALLED,
effects: {
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 53868b7c02d..88505eac3a9 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -36,6 +36,7 @@ export default class ClusterStore {
constructor() {
this.state = {
helpPath: null,
+ helmHelpPath: null,
ingressHelpPath: null,
environmentsHelpPath: null,
clustersHelpPath: null,
@@ -49,7 +50,7 @@ export default class ClusterStore {
applications: {
helm: {
...applicationInitialState,
- title: s__('ClusterIntegration|Helm Tiller'),
+ title: s__('ClusterIntegration|Legacy Helm Tiller server'),
},
ingress: {
...applicationInitialState,
@@ -126,26 +127,10 @@ export default class ClusterStore {
};
}
- setHelpPaths(
- helpPath,
- ingressHelpPath,
- ingressDnsHelpPath,
- ingressModSecurityHelpPath,
- environmentsHelpPath,
- clustersHelpPath,
- deployBoardsHelpPath,
- cloudRunHelpPath,
- ciliumHelpPath,
- ) {
- this.state.helpPath = helpPath;
- this.state.ingressHelpPath = ingressHelpPath;
- this.state.ingressDnsHelpPath = ingressDnsHelpPath;
- this.state.ingressModSecurityHelpPath = ingressModSecurityHelpPath;
- this.state.environmentsHelpPath = environmentsHelpPath;
- this.state.clustersHelpPath = clustersHelpPath;
- this.state.deployBoardsHelpPath = deployBoardsHelpPath;
- this.state.cloudRunHelpPath = cloudRunHelpPath;
- this.state.ciliumHelpPath = ciliumHelpPath;
+ setHelpPaths(helpPaths) {
+ Object.assign(this.state, {
+ ...helpPaths,
+ });
}
setManagePrometheusPath(managePrometheusPath) {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 9d8d184a3f6..09baf16ade9 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -230,9 +230,6 @@ export default {
}
},
diffViewType() {
- if (!this.glFeatures.unifiedDiffLines && (this.needsReload() || this.needsFirstLoad())) {
- this.refetchDiffData();
- }
this.adjustView();
},
shouldShow() {
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 1b747fb7f20..a548354f257 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -136,7 +136,12 @@ export default {
class="d-inline-flex mb-2"
/>
<gl-button-group class="gl-ml-4 gl-mb-4" data-testid="commit-sha-group">
- <gl-button label class="gl-font-monospace" v-text="commit.short_id" />
+ <gl-button
+ label
+ class="gl-font-monospace"
+ data-testid="commit-sha-short-id"
+ v-text="commit.short_id"
+ />
<clipboard-button
:text="commit.id"
:title="__('Copy commit SHA')"
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 401064fb18f..1454728288e 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -87,7 +87,7 @@ export default {
return this.getUserData;
},
mappedLines() {
- if (this.glFeatures.unifiedDiffLines && this.glFeatures.unifiedDiffComponents) {
+ if (this.glFeatures.unifiedDiffComponents) {
return this.diffLines(this.diffFile, true).map(mapParallel(this)) || [];
}
@@ -95,9 +95,7 @@ export default {
if (this.isInlineView) {
return this.diffFile.highlighted_diff_lines.map(mapInline(this));
}
- return this.glFeatures.unifiedDiffLines
- ? this.diffLines(this.diffFile).map(mapParallel(this))
- : this.diffFile.parallel_diff_lines.map(mapParallel(this)) || [];
+ return this.diffLines(this.diffFile).map(mapParallel(this));
},
},
updated() {
@@ -129,9 +127,7 @@ export default {
<template>
<div class="diff-content">
<div class="diff-viewer">
- <template
- v-if="isTextFile && glFeatures.unifiedDiffLines && glFeatures.unifiedDiffComponents"
- >
+ <template v-if="isTextFile && glFeatures.unifiedDiffComponents">
<diff-view
:diff-file="diffFile"
:diff-lines="mappedLines"
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 4c49dfb5de9..ca0edb448b6 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -4,7 +4,7 @@ import { GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { UNFOLD_COUNT, INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
+import { UNFOLD_COUNT, INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_LINES_KEY } from '../constants';
import * as utils from '../store/utils';
const EXPAND_ALL = 0;
@@ -14,7 +14,6 @@ const EXPAND_DOWN = 2;
const lineNumberByViewType = (viewType, diffLine) => {
const numberGetters = {
[INLINE_DIFF_VIEW_TYPE]: line => line?.new_line,
- [PARALLEL_DIFF_VIEW_TYPE]: line => (line?.right || line?.left)?.new_line,
};
const numberGetter = numberGetters[viewType];
return numberGetter && numberGetter(diffLine);
@@ -57,9 +56,6 @@ export default {
},
computed: {
...mapState({
- diffViewType(state) {
- return this.glFeatures.unifiedDiffLines ? INLINE_DIFF_VIEW_TYPE : state.diffs.diffViewType;
- },
diffFiles: state => state.diffs.diffFiles,
}),
canExpandUp() {
@@ -77,16 +73,14 @@ export default {
...mapActions('diffs', ['loadMoreLines']),
getPrevLineNumber(oldLineNumber, newLineNumber) {
const diffFile = utils.findDiffFile(this.diffFiles, this.fileHash);
- const lines = {
- [INLINE_DIFF_VIEW_TYPE]: diffFile.highlighted_diff_lines,
- [PARALLEL_DIFF_VIEW_TYPE]: diffFile.parallel_diff_lines,
- };
- const index = utils.getPreviousLineIndex(this.diffViewType, diffFile, {
+ const index = utils.getPreviousLineIndex(INLINE_DIFF_VIEW_TYPE, diffFile, {
oldLineNumber,
newLineNumber,
});
- return lineNumberByViewType(this.diffViewType, lines[this.diffViewType][index - 2]) || 0;
+ return (
+ lineNumberByViewType(INLINE_DIFF_VIEW_TYPE, diffFile[INLINE_DIFF_LINES_KEY][index - 2]) || 0
+ );
},
callLoadMoreLines(
endpoint,
@@ -113,7 +107,7 @@ export default {
this.isRequesting = true;
const endpoint = this.contextLinesPath;
const { fileHash } = this;
- const view = this.diffViewType;
+ const view = INLINE_DIFF_VIEW_TYPE;
const oldLineNumber = this.line.meta_data.old_pos || 0;
const newLineNumber = this.line.meta_data.new_pos || 0;
const offset = newLineNumber - oldLineNumber;
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 55f5a736cdf..172a2bdde7d 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -7,7 +7,7 @@ import noteForm from '../../notes/components/note_form.vue';
import MultilineCommentForm from '../../notes/components/multiline_comment_form.vue';
import autosave from '../../notes/mixins/autosave';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import { DIFF_NOTE_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
+import { DIFF_NOTE_TYPE, INLINE_DIFF_LINES_KEY, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
import {
commentLineOptions,
formatLineRange,
@@ -102,13 +102,13 @@ export default {
};
const getDiffLines = () => {
if (this.diffViewType === PARALLEL_DIFF_VIEW_TYPE) {
- return (this.glFeatures.unifiedDiffLines
- ? this.diffLines(this.diffFile)
- : this.diffFile.parallel_diff_lines
- ).reduce(combineSides, []);
+ return this.diffLines(this.diffFile, this.glFeatures.unifiedDiffComponents).reduce(
+ combineSides,
+ [],
+ );
}
- return this.diffFile.highlighted_diff_lines;
+ return this.diffFile[INLINE_DIFF_LINES_KEY];
};
const side = this.line.type === 'new' ? 'right' : 'left';
const lines = getDiffLines();
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 91c4c51487f..b3cdf138ac9 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -30,13 +30,11 @@ import {
OLD_LINE_KEY,
NEW_LINE_KEY,
TYPE_KEY,
- LEFT_LINE_KEY,
MAX_RENDERING_DIFF_LINES,
MAX_RENDERING_BULK_ROWS,
MIN_RENDERING_MS,
START_RENDERING_INDEX,
INLINE_DIFF_LINES_KEY,
- PARALLEL_DIFF_LINES_KEY,
DIFFS_PER_PAGE,
DIFF_WHITESPACE_COOKIE_NAME,
SHOW_WHITESPACE,
@@ -77,7 +75,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
const urlParams = {
per_page: DIFFS_PER_PAGE,
w: state.showWhitespace ? '0' : '1',
- view: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
+ view: 'inline',
};
commit(types.SET_BATCH_LOADING, true);
@@ -140,7 +138,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
export const fetchDiffFilesMeta = ({ commit, state }) => {
const worker = new TreeWorker();
const urlParams = {
- view: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
+ view: 'inline',
};
commit(types.SET_LOADING, true);
@@ -401,15 +399,10 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
const lineCodesWithDiscussions = new Set();
- const { parallel_diff_lines: parallelLines, highlighted_diff_lines: inlineLines } = diff;
- const allLines = inlineLines.concat(
- parallelLines.map(line => line.left),
- parallelLines.map(line => line.right),
- );
const lineHasDiscussion = line => Boolean(line?.discussions.length);
const registerDiscussionLine = line => lineCodesWithDiscussions.add(line.line_code);
- allLines.filter(lineHasDiscussion).forEach(registerDiscussionLine);
+ diff[INLINE_DIFF_LINES_KEY].filter(lineHasDiscussion).forEach(registerDiscussionLine);
if (lineCodesWithDiscussions.size) {
Array.from(lineCodesWithDiscussions).forEach(lineCode => {
@@ -508,61 +501,26 @@ export const receiveFullDiffError = ({ commit }, filePath) => {
createFlash(s__('MergeRequest|Error loading full diff. Please try again.'));
};
-export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
- const expandedDiffLines = {
- highlighted_diff_lines: convertExpandLines({
- diffLines: file.highlighted_diff_lines,
- typeKey: TYPE_KEY,
- oldLineKey: OLD_LINE_KEY,
- newLineKey: NEW_LINE_KEY,
- data,
- mapLine: ({ line, oldLine, newLine }) =>
- Object.assign(line, {
- old_line: oldLine,
- new_line: newLine,
- line_code: `${file.file_hash}_${oldLine}_${newLine}`,
- }),
- }),
- parallel_diff_lines: convertExpandLines({
- diffLines: file.parallel_diff_lines,
- typeKey: [LEFT_LINE_KEY, TYPE_KEY],
- oldLineKey: [LEFT_LINE_KEY, OLD_LINE_KEY],
- newLineKey: [LEFT_LINE_KEY, NEW_LINE_KEY],
- data,
- mapLine: ({ line, oldLine, newLine }) => ({
- left: {
- ...line,
- old_line: oldLine,
- line_code: `${file.file_hash}_${oldLine}_${newLine}`,
- },
- right: {
- ...line,
- new_line: newLine,
- line_code: `${file.file_hash}_${newLine}_${oldLine}`,
- },
+export const setExpandedDiffLines = ({ commit }, { file, data }) => {
+ const expandedDiffLines = convertExpandLines({
+ diffLines: file[INLINE_DIFF_LINES_KEY],
+ typeKey: TYPE_KEY,
+ oldLineKey: OLD_LINE_KEY,
+ newLineKey: NEW_LINE_KEY,
+ data,
+ mapLine: ({ line, oldLine, newLine }) =>
+ Object.assign(line, {
+ old_line: oldLine,
+ new_line: newLine,
+ line_code: `${file.file_hash}_${oldLine}_${newLine}`,
}),
- }),
- };
- const unifiedDiffLinesEnabled = window.gon?.features?.unifiedDiffLines;
- const currentDiffLinesKey =
- state.diffViewType === INLINE_DIFF_VIEW_TYPE || unifiedDiffLinesEnabled
- ? INLINE_DIFF_LINES_KEY
- : PARALLEL_DIFF_LINES_KEY;
- const hiddenDiffLinesKey =
- state.diffViewType === INLINE_DIFF_VIEW_TYPE ? PARALLEL_DIFF_LINES_KEY : INLINE_DIFF_LINES_KEY;
-
- if (!unifiedDiffLinesEnabled) {
- commit(types.SET_HIDDEN_VIEW_DIFF_FILE_LINES, {
- filePath: file.file_path,
- lines: expandedDiffLines[hiddenDiffLinesKey],
- });
- }
+ });
- if (expandedDiffLines[currentDiffLinesKey].length > MAX_RENDERING_DIFF_LINES) {
+ if (expandedDiffLines.length > MAX_RENDERING_DIFF_LINES) {
let index = START_RENDERING_INDEX;
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, {
filePath: file.file_path,
- lines: expandedDiffLines[currentDiffLinesKey].slice(0, index),
+ lines: expandedDiffLines.slice(0, index),
});
commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path);
@@ -571,10 +529,10 @@ export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
while (
t.timeRemaining() >= MIN_RENDERING_MS &&
- index !== expandedDiffLines[currentDiffLinesKey].length &&
+ index !== expandedDiffLines.length &&
index - startIndex !== MAX_RENDERING_BULK_ROWS
) {
- const line = expandedDiffLines[currentDiffLinesKey][index];
+ const line = expandedDiffLines[index];
if (line) {
commit(types.ADD_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: file.file_path, line });
@@ -582,7 +540,7 @@ export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
}
}
- if (index !== expandedDiffLines[currentDiffLinesKey].length) {
+ if (index !== expandedDiffLines.length) {
idleCallback(idleCb);
} else {
commit(types.TOGGLE_DIFF_FILE_RENDERING_MORE, file.file_path);
@@ -593,7 +551,7 @@ export const setExpandedDiffLines = ({ commit, state }, { file, data }) => {
} else {
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, {
filePath: file.file_path,
- lines: expandedDiffLines[currentDiffLinesKey],
+ lines: expandedDiffLines,
});
}
};
@@ -627,7 +585,7 @@ export const toggleFullDiff = ({ dispatch, commit, getters, state }, filePath) =
}
};
-export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { diffFile }) {
+export function switchToFullDiffFromRenamedFile({ commit, dispatch }, { diffFile }) {
return axios
.get(diffFile.context_lines_path, {
params: {
@@ -638,7 +596,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
.then(({ data }) => {
const lines = data.map((line, index) =>
prepareLineForRenamedFile({
- diffViewType: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
+ diffViewType: 'inline',
line,
diffFile,
index,
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 9ee73998177..baf54188932 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,6 +1,10 @@
import { __, n__ } from '~/locale';
import { parallelizeDiffLines } from './utils';
-import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
+import {
+ PARALLEL_DIFF_VIEW_TYPE,
+ INLINE_DIFF_VIEW_TYPE,
+ INLINE_DIFF_LINES_KEY,
+} from '../constants';
export * from './getters_versions_dropdowns';
@@ -54,24 +58,10 @@ export const diffHasAllCollapsedDiscussions = (state, getters) => diff => {
* @param {Object} diff
* @returns {Boolean}
*/
-export const diffHasExpandedDiscussions = state => diff => {
- const lines = {
- [INLINE_DIFF_VIEW_TYPE]: diff.highlighted_diff_lines || [],
- [PARALLEL_DIFF_VIEW_TYPE]: (diff.parallel_diff_lines || []).reduce((acc, line) => {
- if (line.left) {
- acc.push(line.left);
- }
-
- if (line.right) {
- acc.push(line.right);
- }
-
- return acc;
- }, []),
- };
- return lines[window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType]
- .filter(l => l.discussions.length >= 1)
- .some(l => l.discussionsExpanded);
+export const diffHasExpandedDiscussions = () => diff => {
+ return diff[INLINE_DIFF_LINES_KEY].filter(l => l.discussions.length >= 1).some(
+ l => l.discussionsExpanded,
+ );
};
/**
@@ -79,24 +69,8 @@ export const diffHasExpandedDiscussions = state => diff => {
* @param {Boolean} diff
* @returns {Boolean}
*/
-export const diffHasDiscussions = state => diff => {
- const lines = {
- [INLINE_DIFF_VIEW_TYPE]: diff.highlighted_diff_lines || [],
- [PARALLEL_DIFF_VIEW_TYPE]: (diff.parallel_diff_lines || []).reduce((acc, line) => {
- if (line.left) {
- acc.push(line.left);
- }
-
- if (line.right) {
- acc.push(line.right);
- }
-
- return acc;
- }, []),
- };
- return lines[window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType].some(
- l => l.discussions.length >= 1,
- );
+export const diffHasDiscussions = () => diff => {
+ return diff[INLINE_DIFF_LINES_KEY].some(l => l.discussions.length >= 1);
};
/**
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 19a9e65edc9..3223d61e48b 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -35,7 +35,6 @@ export const RECEIVE_FULL_DIFF_SUCCESS = 'RECEIVE_FULL_DIFF_SUCCESS';
export const RECEIVE_FULL_DIFF_ERROR = 'RECEIVE_FULL_DIFF_ERROR';
export const SET_FILE_COLLAPSED = 'SET_FILE_COLLAPSED';
-export const SET_HIDDEN_VIEW_DIFF_FILE_LINES = 'SET_HIDDEN_VIEW_DIFF_FILE_LINES';
export const SET_CURRENT_VIEW_DIFF_FILE_LINES = 'SET_CURRENT_VIEW_DIFF_FILE_LINES';
export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINES';
export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 096c4f69439..c5bb2b40163 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -1,11 +1,6 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
- DIFF_FILE_MANUAL_COLLAPSE,
- DIFF_FILE_AUTOMATIC_COLLAPSE,
- INLINE_DIFF_VIEW_TYPE,
-} from '../constants';
-import {
findDiffFile,
addLineReferences,
removeMatchLine,
@@ -14,6 +9,11 @@ import {
isDiscussionApplicableToLine,
updateLineInFile,
} from './utils';
+import {
+ DIFF_FILE_MANUAL_COLLAPSE,
+ DIFF_FILE_AUTOMATIC_COLLAPSE,
+ INLINE_DIFF_LINES_KEY,
+} from '../constants';
import * as types from './mutation_types';
function updateDiffFilesInState(state, files) {
@@ -109,25 +109,7 @@ export default {
if (!diffFile) return;
- if (diffFile.highlighted_diff_lines.length) {
- diffFile.highlighted_diff_lines.find(l => l.line_code === lineCode).hasForm = hasForm;
- }
-
- if (diffFile.parallel_diff_lines.length) {
- const line = diffFile.parallel_diff_lines.find(l => {
- const { left, right } = l;
-
- return (left && left.line_code === lineCode) || (right && right.line_code === lineCode);
- });
-
- if (line.left && line.left.line_code === lineCode) {
- line.left.hasForm = hasForm;
- }
-
- if (line.right && line.right.line_code === lineCode) {
- line.right.hasForm = hasForm;
- }
- }
+ diffFile[INLINE_DIFF_LINES_KEY].find(l => l.line_code === lineCode).hasForm = hasForm;
},
[types.ADD_CONTEXT_LINES](state, options) {
@@ -157,11 +139,7 @@ export default {
});
addContextLines({
- inlineLines: diffFile.highlighted_diff_lines,
- parallelLines: diffFile.parallel_diff_lines,
- diffViewType: window.gon?.features?.unifiedDiffLines
- ? INLINE_DIFF_VIEW_TYPE
- : state.diffViewType,
+ inlineLines: diffFile[INLINE_DIFF_LINES_KEY],
contextLines: lines,
bottom,
lineNumbers,
@@ -219,8 +197,8 @@ export default {
state.diffFiles.forEach(file => {
if (file.file_hash === fileHash) {
- if (file.highlighted_diff_lines.length) {
- file.highlighted_diff_lines.forEach(line => {
+ if (file[INLINE_DIFF_LINES_KEY].length) {
+ file[INLINE_DIFF_LINES_KEY].forEach(line => {
Object.assign(
line,
setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
@@ -228,25 +206,7 @@ export default {
});
}
- if (file.parallel_diff_lines.length) {
- file.parallel_diff_lines.forEach(line => {
- const left = line.left && lineCheck(line.left);
- const right = line.right && lineCheck(line.right);
-
- if (left || right) {
- Object.assign(line, {
- left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
- right: line.right
- ? setDiscussionsExpanded(mapDiscussions(line.right, () => !left))
- : null,
- });
- }
-
- return line;
- });
- }
-
- if (!file.parallel_diff_lines.length || !file.highlighted_diff_lines.length) {
+ if (!file[INLINE_DIFF_LINES_KEY].length) {
const newDiscussions = (file.discussions || [])
.filter(d => d.id !== discussion.id)
.concat(discussion);
@@ -369,31 +329,15 @@ export default {
renderFile(file);
}
},
- [types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
- const file = state.diffFiles.find(f => f.file_path === filePath);
- const hiddenDiffLinesKey =
- state.diffViewType === 'inline' ? 'parallel_diff_lines' : 'highlighted_diff_lines';
-
- file[hiddenDiffLinesKey] = lines;
- },
[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
const file = state.diffFiles.find(f => f.file_path === filePath);
- let currentDiffLinesKey;
-
- if (window.gon?.features?.unifiedDiffLines || state.diffViewType === 'inline') {
- currentDiffLinesKey = 'highlighted_diff_lines';
- } else {
- currentDiffLinesKey = 'parallel_diff_lines';
- }
- file[currentDiffLinesKey] = lines;
+ file[INLINE_DIFF_LINES_KEY] = lines;
},
[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, line }) {
const file = state.diffFiles.find(f => f.file_path === filePath);
- const currentDiffLinesKey =
- state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines';
- file[currentDiffLinesKey].push(line);
+ file[INLINE_DIFF_LINES_KEY].push(line);
},
[types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, filePath) {
const file = state.diffFiles.find(f => f.file_path === filePath);
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index f87f57c32c3..509a89d52f6 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -12,8 +12,7 @@ import {
MATCH_LINE_TYPE,
LINES_TO_BE_RENDERED_DIRECTLY,
TREE_TYPE,
- INLINE_DIFF_VIEW_TYPE,
- PARALLEL_DIFF_VIEW_TYPE,
+ INLINE_DIFF_LINES_KEY,
SHOW_WHITESPACE,
NO_SHOW_WHITESPACE,
} from '../constants';
@@ -178,43 +177,16 @@ export const findIndexInInlineLines = (lines, lineNumbers) => {
);
};
-export const findIndexInParallelLines = (lines, lineNumbers) => {
- const { oldLineNumber, newLineNumber } = lineNumbers;
-
- return lines.findIndex(
- line =>
- line.left &&
- line.right &&
- line.left.old_line === oldLineNumber &&
- line.right.new_line === newLineNumber,
- );
-};
-
-const indexGettersByViewType = {
- [INLINE_DIFF_VIEW_TYPE]: findIndexInInlineLines,
- [PARALLEL_DIFF_VIEW_TYPE]: findIndexInParallelLines,
-};
-
export const getPreviousLineIndex = (diffViewType, file, lineNumbers) => {
- const findIndex = indexGettersByViewType[diffViewType];
- const lines = {
- [INLINE_DIFF_VIEW_TYPE]: file.highlighted_diff_lines,
- [PARALLEL_DIFF_VIEW_TYPE]: file.parallel_diff_lines,
- };
-
- return findIndex && findIndex(lines[diffViewType], lineNumbers);
+ return findIndexInInlineLines(file[INLINE_DIFF_LINES_KEY], lineNumbers);
};
export function removeMatchLine(diffFile, lineNumbers, bottom) {
- const indexForInline = findIndexInInlineLines(diffFile.highlighted_diff_lines, lineNumbers);
- const indexForParallel = findIndexInParallelLines(diffFile.parallel_diff_lines, lineNumbers);
+ const indexForInline = findIndexInInlineLines(diffFile[INLINE_DIFF_LINES_KEY], lineNumbers);
const factor = bottom ? 1 : -1;
if (indexForInline > -1) {
- diffFile.highlighted_diff_lines.splice(indexForInline + factor, 1);
- }
- if (indexForParallel > -1) {
- diffFile.parallel_diff_lines.splice(indexForParallel + factor, 1);
+ diffFile[INLINE_DIFF_LINES_KEY].splice(indexForInline + factor, 1);
}
}
@@ -257,24 +229,6 @@ export function addLineReferences(lines, lineNumbers, bottom, isExpandDown, next
return linesWithNumbers;
}
-function addParallelContextLines(options) {
- const { parallelLines, contextLines, lineNumbers, isExpandDown } = options;
- const normalizedParallelLines = contextLines.map(line => ({
- left: line,
- right: line,
- line_code: line.line_code,
- }));
- const factor = isExpandDown ? 1 : 0;
-
- if (!isExpandDown && options.bottom) {
- parallelLines.push(...normalizedParallelLines);
- } else {
- const parallelIndex = findIndexInParallelLines(parallelLines, lineNumbers);
-
- parallelLines.splice(parallelIndex + factor, 0, ...normalizedParallelLines);
- }
-}
-
function addInlineContextLines(options) {
const { inlineLines, contextLines, lineNumbers, isExpandDown } = options;
const factor = isExpandDown ? 1 : 0;
@@ -289,16 +243,7 @@ function addInlineContextLines(options) {
}
export function addContextLines(options) {
- const { diffViewType } = options;
- const contextLineHandlers = {
- [INLINE_DIFF_VIEW_TYPE]: addInlineContextLines,
- [PARALLEL_DIFF_VIEW_TYPE]: addParallelContextLines,
- };
- const contextLineHandler = contextLineHandlers[diffViewType];
-
- if (contextLineHandler) {
- contextLineHandler(options);
- }
+ addInlineContextLines(options);
}
/**
@@ -324,41 +269,29 @@ export function trimFirstCharOfLineContent(line = {}) {
return parsedLine;
}
-function getLineCode({ left, right }, index) {
- if (left && left.line_code) {
- return left.line_code;
- } else if (right && right.line_code) {
- return right.line_code;
- }
- return index;
-}
-
function diffFileUniqueId(file) {
return `${file.content_sha}-${file.file_hash}`;
}
function mergeTwoFiles(target, source) {
- const originalInline = target.highlighted_diff_lines;
- const originalParallel = target.parallel_diff_lines;
+ const originalInline = target[INLINE_DIFF_LINES_KEY];
const missingInline = !originalInline.length;
- const missingParallel = !originalParallel.length;
return {
...target,
- highlighted_diff_lines: missingInline ? source.highlighted_diff_lines : originalInline,
- parallel_diff_lines: missingParallel ? source.parallel_diff_lines : originalParallel,
+ [INLINE_DIFF_LINES_KEY]: missingInline ? source[INLINE_DIFF_LINES_KEY] : originalInline,
+ parallel_diff_lines: null,
renderIt: source.renderIt,
collapsed: source.collapsed,
};
}
function ensureBasicDiffFileLines(file) {
- const missingInline = !file.highlighted_diff_lines;
- const missingParallel = !file.parallel_diff_lines || window.gon?.features?.unifiedDiffLines;
+ const missingInline = !file[INLINE_DIFF_LINES_KEY];
Object.assign(file, {
- highlighted_diff_lines: missingInline ? [] : file.highlighted_diff_lines,
- parallel_diff_lines: missingParallel ? [] : file.parallel_diff_lines,
+ [INLINE_DIFF_LINES_KEY]: missingInline ? [] : file[INLINE_DIFF_LINES_KEY],
+ parallel_diff_lines: null,
});
return file;
@@ -382,7 +315,7 @@ function prepareLine(line, file) {
}
}
-export function prepareLineForRenamedFile({ line, diffViewType, diffFile, index = 0 }) {
+export function prepareLineForRenamedFile({ line, diffFile, index = 0 }) {
/*
Renamed files are a little different than other diffs, which
is why this is distinct from `prepareDiffFileLines` below.
@@ -407,48 +340,23 @@ export function prepareLineForRenamedFile({ line, diffViewType, diffFile, index
prepareLine(cleanLine, diffFile); // WARNING: In-Place Mutations!
- if (diffViewType === PARALLEL_DIFF_VIEW_TYPE) {
- return {
- left: { ...cleanLine },
- right: { ...cleanLine },
- line_code: cleanLine.line_code,
- };
- }
-
return cleanLine;
}
function prepareDiffFileLines(file) {
- const inlineLines = file.highlighted_diff_lines;
- const parallelLines = file.parallel_diff_lines;
- let parallelLinesCount = 0;
+ const inlineLines = file[INLINE_DIFF_LINES_KEY];
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, file); // WARNING: In-Place Mutations!
- }
-
- if (line.right) {
- parallelLinesCount += 1;
- prepareLine(line.right, file); // WARNING: In-Place Mutations!
- }
- });
-
Object.assign(file, {
inlineLinesCount: inlineLines.length,
- parallelLinesCount,
});
return file;
}
function getVisibleDiffLines(file) {
- return Math.max(file.inlineLinesCount, file.parallelLinesCount);
+ return file.inlineLinesCount;
}
function finalizeDiffFile(file) {
@@ -490,43 +398,14 @@ export function prepareDiffData(diff, priorFiles = []) {
export function getDiffPositionByLineCode(diffFiles) {
let lines = [];
- const hasInlineDiffs = diffFiles.some(file => file.highlighted_diff_lines.length > 0);
-
- if (hasInlineDiffs) {
- // In either of these cases, we can use `highlighted_diff_lines` because
- // that will include all of the parallel diff lines, too
-
- lines = diffFiles.reduce((acc, diffFile) => {
- diffFile.highlighted_diff_lines.forEach(line => {
- acc.push({ file: diffFile, line });
- });
-
- return acc;
- }, []);
- } else {
- // If we're in single diff view mode and the inline lines haven't been
- // loaded yet, we need to parse the parallel lines
-
- lines = diffFiles.reduce((acc, diffFile) => {
- diffFile.parallel_diff_lines.forEach(pair => {
- // It's possible for a parallel line to have an opposite line that doesn't exist
- // For example: *deleted* lines will have `null` right lines, while
- // *added* lines will have `null` left lines.
- // So we have to check each line before we push it onto the array so we're not
- // pushing null line diffs
-
- if (pair.left) {
- acc.push({ file: diffFile, line: pair.left });
- }
- if (pair.right) {
- acc.push({ file: diffFile, line: pair.right });
- }
- });
+ lines = diffFiles.reduce((acc, diffFile) => {
+ diffFile[INLINE_DIFF_LINES_KEY].forEach(line => {
+ acc.push({ file: diffFile, line });
+ });
- return acc;
- }, []);
- }
+ return acc;
+ }, []);
return lines.reduce((acc, { file, line }) => {
if (line.line_code) {
@@ -739,24 +618,10 @@ export const convertExpandLines = ({
export const idleCallback = cb => requestIdleCallback(cb);
function getLinesFromFileByLineCode(file, lineCode) {
- const parallelLines = file.parallel_diff_lines;
- const inlineLines = file.highlighted_diff_lines;
+ const inlineLines = file[INLINE_DIFF_LINES_KEY];
const matchesCode = line => line.line_code === lineCode;
- return [
- ...parallelLines.reduce((acc, line) => {
- if (line.left) {
- acc.push(line.left);
- }
-
- if (line.right) {
- acc.push(line.right);
- }
-
- return acc;
- }, []),
- ...inlineLines,
- ].filter(matchesCode);
+ return inlineLines.filter(matchesCode);
}
export const updateLineInFile = (selectedFile, lineCode, updateFn) => {
@@ -771,12 +636,7 @@ export const allDiscussionWrappersExpanded = diff => {
}
};
- diff.parallel_diff_lines.forEach(line => {
- changeExpandedResult(line.left);
- changeExpandedResult(line.right);
- });
-
- diff.highlighted_diff_lines.forEach(line => {
+ diff[INLINE_DIFF_LINES_KEY].forEach(line => {
changeExpandedResult(line);
});
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index bc35a07fe4a..2192d456861 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { formatTime } from '~/lib/utils/datetime_utility';
import eventHub from '../event_hub';
@@ -9,7 +9,8 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- GlButton,
+ GlDropdown,
+ GlDropdownItem,
GlIcon,
GlLoadingIcon,
},
@@ -35,7 +36,7 @@ export default {
if (action.scheduledAt) {
const confirmationMessage = sprintf(
s__(
- "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
+ 'DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes.',
),
{ jobName: action.name },
);
@@ -67,40 +68,32 @@ export default {
};
</script>
<template>
- <div class="btn-group" role="group">
- <gl-button
- v-gl-tooltip
- :title="title"
- :aria-label="title"
- :disabled="isLoading"
- class="dropdown dropdown-new js-environment-actions-dropdown"
- data-container="body"
- data-toggle="dropdown"
- data-testid="environment-actions-button"
+ <gl-dropdown
+ v-gl-tooltip
+ :title="title"
+ :aria-label="title"
+ :disabled="isLoading"
+ right
+ data-container="body"
+ data-testid="environment-actions-button"
+ >
+ <template #button-content>
+ <gl-icon name="play" />
+ <gl-icon name="chevron-down" />
+ <gl-loading-icon v-if="isLoading" />
+ </template>
+ <gl-dropdown-item
+ v-for="(action, i) in actions"
+ :key="i"
+ :disabled="isActionDisabled(action)"
+ data-testid="manual-action-link"
+ @click="onClickAction(action)"
>
- <span>
- <gl-icon name="play" />
- <gl-icon name="chevron-down" />
- <gl-loading-icon v-if="isLoading" />
+ <span class="gl-flex-fill-1">{{ action.name }}</span>
+ <span v-if="action.scheduledAt" class="gl-text-gray-500 float-right">
+ <gl-icon name="clock" />
+ {{ remainingTime(action) }}
</span>
- </gl-button>
-
- <ul class="dropdown-menu dropdown-menu-right">
- <li v-for="(action, i) in actions" :key="i" class="gl-display-flex">
- <gl-button
- :class="{ disabled: isActionDisabled(action) }"
- :disabled="isActionDisabled(action)"
- variant="link"
- class="js-manual-action-link gl-flex-fill-1"
- @click="onClickAction(action)"
- >
- <span class="gl-flex-fill-1">{{ action.name }}</span>
- <span v-if="action.scheduledAt" class="text-secondary float-right">
- <gl-icon name="clock" />
- {{ remainingTime(action) }}
- </span>
- </gl-button>
- </li>
- </ul>
- </div>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 6e99b6ad4fa..ef58b93c049 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -74,6 +74,9 @@ export default {
visibilityTooltip() {
return GROUP_VISIBILITY_TYPE[this.group.visibility];
},
+ microdata() {
+ return this.group.microdata || {};
+ },
},
mounted() {
if (this.group.name === 'Learn GitLab') {
@@ -99,7 +102,15 @@ export default {
</script>
<template>
- <li :id="groupDomId" :class="rowClass" class="group-row" @click.stop="onClickRowGroup">
+ <li
+ :id="groupDomId"
+ :class="rowClass"
+ class="group-row"
+ :itemprop="microdata.itemprop"
+ :itemtype="microdata.itemtype"
+ :itemscope="microdata.itemscope"
+ @click.stop="onClickRowGroup"
+ >
<div
:class="{ 'project-row-contents': !isGroup }"
class="group-row-contents d-flex align-items-center py-2 pr-3"
@@ -118,7 +129,13 @@ export default {
class="avatar-container rect-avatar s32 d-none flex-grow-0 flex-shrink-0 "
>
<a :href="group.relativePath" class="no-expand">
- <img v-if="hasAvatar" :src="group.avatarUrl" class="avatar s40" />
+ <img
+ v-if="hasAvatar"
+ :src="group.avatarUrl"
+ data-testid="group-avatar"
+ class="avatar s40"
+ :itemprop="microdata.imageItemprop"
+ />
<identicon v-else :entity-id="group.id" :entity-name="group.name" size-class="s40" />
</a>
</div>
@@ -127,9 +144,11 @@ export default {
<div class="d-flex align-items-center flex-wrap title namespace-title gl-mr-3">
<a
v-gl-tooltip.bottom
+ data-testid="group-name"
:href="group.relativePath"
:title="group.fullName"
class="no-expand gl-mt-3 gl-mr-3 gl-text-gray-900!"
+ :itemprop="microdata.nameItemprop"
>{{
// ending bracket must be by closing tag to prevent
// link hover text-decoration from over-extending
@@ -146,7 +165,12 @@ export default {
</span>
</div>
<div v-if="group.description" class="description">
- <span v-html="group.description"> </span>
+ <span
+ :itemprop="microdata.descriptionItemprop"
+ data-testid="group-description"
+ v-html="group.description"
+ >
+ </span>
</div>
</div>
<div v-if="isGroupPendingRemoval">
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index 522f1d16df2..e11c3aaf984 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -47,8 +47,9 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
data() {
const { dataset } = dataEl || this.$options.el;
const hideProjects = parseBoolean(dataset.hideProjects);
+ const showSchemaMarkup = parseBoolean(dataset.showSchemaMarkup);
const service = new GroupsService(endpoint || dataset.endpoint);
- const store = new GroupsStore(hideProjects);
+ const store = new GroupsStore({ hideProjects, showSchemaMarkup });
return {
action,
diff --git a/app/assets/javascripts/groups/members/components/app.vue b/app/assets/javascripts/groups/members/components/app.vue
index 2e6dd4a0bad..8f1bb6e8094 100644
--- a/app/assets/javascripts/groups/members/components/app.vue
+++ b/app/assets/javascripts/groups/members/components/app.vue
@@ -1,9 +1,9 @@
<script>
import { mapState, mapMutations } from 'vuex';
import { GlAlert } from '@gitlab/ui';
-import MembersTable from '~/vue_shared/components/members/table/members_table.vue';
+import MembersTable from '~/members/components/table/members_table.vue';
import { scrollToElement } from '~/lib/utils/common_utils';
-import { HIDE_ERROR } from '~/vuex_shared/modules/members/mutation_types';
+import { HIDE_ERROR } from '~/members/store/mutation_types';
export default {
name: 'GroupMembersApp',
diff --git a/app/assets/javascripts/groups/members/index.js b/app/assets/javascripts/groups/members/index.js
index cb28fb057c9..68caf6628f6 100644
--- a/app/assets/javascripts/groups/members/index.js
+++ b/app/assets/javascripts/groups/members/index.js
@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import { GlToast } from '@gitlab/ui';
import { parseDataAttributes } from 'ee_else_ce/groups/members/utils';
import App from './components/app.vue';
-import membersModule from '~/vuex_shared/modules/members';
+import membersStore from '~/members/store';
export const initGroupMembersApp = (el, tableFields, tableAttrs, requestFormatter) => {
if (!el) {
@@ -13,15 +13,15 @@ export const initGroupMembersApp = (el, tableFields, tableAttrs, requestFormatte
Vue.use(Vuex);
Vue.use(GlToast);
- const store = new Vuex.Store({
- ...membersModule({
+ const store = new Vuex.Store(
+ membersStore({
...parseDataAttributes(el),
currentUserId: gon.current_user_id || null,
tableFields,
tableAttrs,
requestFormatter,
}),
- });
+ );
return new Vue({
el,
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
index 6a1197fa163..b6cea38e87f 100644
--- a/app/assets/javascripts/groups/store/groups_store.js
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -1,11 +1,13 @@
import { normalizeHeaders, parseIntPagination } from '../../lib/utils/common_utils';
+import { getGroupItemMicrodata } from './utils';
export default class GroupsStore {
- constructor(hideProjects) {
+ constructor({ hideProjects = false, showSchemaMarkup = false } = {}) {
this.state = {};
this.state.groups = [];
this.state.pageInfo = {};
this.hideProjects = hideProjects;
+ this.showSchemaMarkup = showSchemaMarkup;
}
setGroups(rawGroups) {
@@ -94,6 +96,7 @@ export default class GroupsStore {
starCount: rawGroupItem.star_count,
updatedAt: rawGroupItem.updated_at,
pendingRemoval: rawGroupItem.marked_for_deletion,
+ microdata: this.showSchemaMarkup ? getGroupItemMicrodata(rawGroupItem) : {},
};
}
diff --git a/app/assets/javascripts/groups/store/utils.js b/app/assets/javascripts/groups/store/utils.js
new file mode 100644
index 00000000000..371b3aa9d52
--- /dev/null
+++ b/app/assets/javascripts/groups/store/utils.js
@@ -0,0 +1,27 @@
+export const getGroupItemMicrodata = ({ type }) => {
+ const defaultMicrodata = {
+ itemscope: true,
+ itemtype: 'https://schema.org/Thing',
+ itemprop: 'owns',
+ imageItemprop: 'image',
+ nameItemprop: 'name',
+ descriptionItemprop: 'description',
+ };
+
+ switch (type) {
+ case 'group':
+ return {
+ ...defaultMicrodata,
+ itemtype: 'https://schema.org/Organization',
+ itemprop: 'subOrganization',
+ imageItemprop: 'logo',
+ };
+ case 'project':
+ return {
+ ...defaultMicrodata,
+ itemtype: 'https://schema.org/SoftwareSourceCode',
+ };
+ default:
+ return defaultMicrodata;
+ }
+};
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index aac23db8fd6..29af8c77d25 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -4,97 +4,107 @@ import axios from './lib/utils/axios_utils';
import Api from './api';
import { normalizeHeaders } from './lib/utils/common_utils';
import { __ } from '~/locale';
+import { loadCSSFile } from './lib/utils/css_utils';
+
+const fetchGroups = params => {
+ axios[params.type.toLowerCase()](params.url, {
+ params: params.data,
+ })
+ .then(res => {
+ const results = res.data || [];
+ const headers = normalizeHeaders(res.headers);
+ const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
+ const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
+ const more = currentPage < totalPages;
+
+ params.success({
+ results,
+ pagination: {
+ more,
+ },
+ });
+ })
+ .catch(params.error);
+};
const groupsSelect = () => {
- // Needs to be accessible in rspec
- window.GROUP_SELECT_PER_PAGE = 20;
- $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
- const $select = $(this);
- const allAvailable = $select.data('allAvailable');
- const skipGroups = $select.data('skipGroups') || [];
- const parentGroupID = $select.data('parentId');
- const groupsPath = parentGroupID
- ? Api.subgroupsPath.replace(':id', parentGroupID)
- : Api.groupsPath;
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ // Needs to be accessible in rspec
+ window.GROUP_SELECT_PER_PAGE = 20;
- $select.select2({
- placeholder: __('Search for a group'),
- allowClear: $select.hasClass('allowClear'),
- multiple: $select.hasClass('multiselect'),
- minimumInputLength: 0,
- ajax: {
- url: Api.buildUrl(groupsPath),
- dataType: 'json',
- quietMillis: 250,
- transport(params) {
- axios[params.type.toLowerCase()](params.url, {
- params: params.data,
- })
- .then(res => {
- const results = res.data || [];
- const headers = normalizeHeaders(res.headers);
- const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
- const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
- const more = currentPage < totalPages;
+ $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
+ const $select = $(this);
+ const allAvailable = $select.data('allAvailable');
+ const skipGroups = $select.data('skipGroups') || [];
+ const parentGroupID = $select.data('parentId');
+ const groupsPath = parentGroupID
+ ? Api.subgroupsPath.replace(':id', parentGroupID)
+ : Api.groupsPath;
- params.success({
- results,
- pagination: {
- more,
- },
- });
- })
- .catch(params.error);
- },
- data(search, page) {
- return {
- search,
- page,
- per_page: window.GROUP_SELECT_PER_PAGE,
- all_available: allAvailable,
- };
- },
- results(data, page) {
- if (data.length) return { results: [] };
+ $select.select2({
+ placeholder: __('Search for a group'),
+ allowClear: $select.hasClass('allowClear'),
+ multiple: $select.hasClass('multiselect'),
+ minimumInputLength: 0,
+ ajax: {
+ url: Api.buildUrl(groupsPath),
+ dataType: 'json',
+ quietMillis: 250,
+ transport(params) {
+ fetchGroups(params);
+ },
+ data(search, page) {
+ return {
+ search,
+ page,
+ per_page: window.GROUP_SELECT_PER_PAGE,
+ all_available: allAvailable,
+ };
+ },
+ results(data, page) {
+ if (data.length) return { results: [] };
- const groups = data.length ? data : data.results || [];
- const more = data.pagination ? data.pagination.more : false;
- const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
+ const groups = data.length ? data : data.results || [];
+ const more = data.pagination ? data.pagination.more : false;
+ const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
- return {
- results,
- page,
- more,
- };
- },
- },
- // eslint-disable-next-line consistent-return
- initSelection(element, callback) {
- const id = $(element).val();
- if (id !== '') {
- return Api.group(id, callback);
- }
- },
- formatResult(object) {
- return `<div class='group-result'> <div class='group-name'>${escape(
- object.full_name,
- )}</div> <div class='group-path'>${object.full_path}</div> </div>`;
- },
- formatSelection(object) {
- return escape(object.full_name);
- },
- dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
+ return {
+ results,
+ page,
+ more,
+ };
+ },
+ },
+ // eslint-disable-next-line consistent-return
+ initSelection(element, callback) {
+ const id = $(element).val();
+ if (id !== '') {
+ return Api.group(id, callback);
+ }
+ },
+ formatResult(object) {
+ return `<div class='group-result'> <div class='group-name'>${escape(
+ object.full_name,
+ )}</div> <div class='group-path'>${object.full_path}</div> </div>`;
+ },
+ formatSelection(object) {
+ return escape(object.full_name);
+ },
+ dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
+ });
- $select.on('select2-loaded', () => {
- const dropdown = document.querySelector('.select2-infinite .select2-results');
- dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
- });
- });
+ $select.on('select2-loaded', () => {
+ const dropdown = document.querySelector('.select2-infinite .select2-results');
+ dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
+ });
+ });
+ })
+ .catch(() => {});
};
export default () => {
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index dec8aa61838..52593aabfea 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,11 +1,12 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { viewerTypes } from '../constants';
export default {
components: {
- GlButton,
+ GlDropdown,
+ GlDropdownItem,
},
props: {
viewer: {
@@ -18,10 +19,21 @@ export default {
},
},
computed: {
- mergeReviewLine() {
- return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
- mergeRequestId: this.mergeRequestId,
- });
+ modeDropdownItems() {
+ return [
+ {
+ viewerType: this.$options.viewerTypes.mr,
+ title: sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
+ mergeRequestId: this.mergeRequestId,
+ }),
+ content: __('Compare changes with the merge request target branch'),
+ },
+ {
+ viewerType: this.$options.viewerTypes.diff,
+ title: __('Reviewing'),
+ content: __('Compare changes with the last commit'),
+ },
+ ];
},
},
methods: {
@@ -34,39 +46,16 @@ export default {
</script>
<template>
- <div class="dropdown">
- <gl-button variant="link" data-toggle="dropdown">{{ __('Edit') }}</gl-button>
- <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
- <ul>
- <li>
- <a
- :class="{
- 'is-active': viewer === $options.viewerTypes.mr,
- }"
- href="#"
- @click.prevent="changeMode($options.viewerTypes.mr)"
- >
- <strong class="dropdown-menu-inner-title"> {{ mergeReviewLine }} </strong>
- <span class="dropdown-menu-inner-content">
- {{ __('Compare changes with the merge request target branch') }}
- </span>
- </a>
- </li>
- <li>
- <a
- :class="{
- 'is-active': viewer === $options.viewerTypes.diff,
- }"
- href="#"
- @click.prevent="changeMode($options.viewerTypes.diff)"
- >
- <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
- <span class="dropdown-menu-inner-content">
- {{ __('Compare changes with the last commit') }}
- </span>
- </a>
- </li>
- </ul>
- </div>
- </div>
+ <gl-dropdown :text="__('Edit')" size="small">
+ <gl-dropdown-item
+ v-for="mode in modeDropdownItems"
+ :key="mode.viewerType"
+ :is-check-item="true"
+ :is-checked="viewer === mode.viewerType"
+ @click="changeMode(mode.viewerType)"
+ >
+ <strong class="dropdown-menu-inner-title"> {{ mode.title }} </strong>
+ <span class="dropdown-menu-inner-content"> {{ mode.content }} </span>
+ </gl-dropdown-item>
+ </gl-dropdown>
</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index e1d2895831a..f1dc855362b 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -5,10 +5,8 @@ import {
WEBIDE_MARK_APP_START,
WEBIDE_MARK_FILE_FINISH,
WEBIDE_MARK_FILE_CLICKED,
- WEBIDE_MARK_TREE_FINISH,
- WEBIDE_MEASURE_TREE_FROM_REQUEST,
- WEBIDE_MEASURE_FILE_FROM_REQUEST,
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
+ WEBIDE_MEASURE_BEFORE_VUE,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import { modalTypes } from '../constants';
@@ -19,12 +17,6 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { measurePerformance } from '../utils';
-eventHub.$on(WEBIDE_MEASURE_TREE_FROM_REQUEST, () =>
- measurePerformance(WEBIDE_MARK_TREE_FINISH, WEBIDE_MEASURE_TREE_FROM_REQUEST),
-);
-eventHub.$on(WEBIDE_MEASURE_FILE_FROM_REQUEST, () =>
- measurePerformance(WEBIDE_MARK_FILE_FINISH, WEBIDE_MEASURE_FILE_FROM_REQUEST),
-);
eventHub.$on(WEBIDE_MEASURE_FILE_AFTER_INTERACTION, () =>
measurePerformance(
WEBIDE_MARK_FILE_FINISH,
@@ -84,7 +76,14 @@ export default {
document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`);
},
beforeCreate() {
- performanceMarkAndMeasure({ mark: WEBIDE_MARK_APP_START });
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_APP_START,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_BEFORE_VUE,
+ },
+ ],
+ });
},
methods: {
...mapActions(['toggleFileFinder']),
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index e7e94f5b5da..b67881b14f4 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -2,17 +2,13 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import FileTree from '~/vue_shared/components/file_tree.vue';
-import {
- WEBIDE_MARK_TREE_START,
- WEBIDE_MEASURE_TREE_FROM_REQUEST,
- WEBIDE_MARK_FILE_CLICKED,
-} from '~/performance/constants';
+import { WEBIDE_MARK_FILE_CLICKED } from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
-import eventHub from '../eventhub';
import IdeFileRow from './ide_file_row.vue';
import NavDropdown from './nav_dropdown.vue';
export default {
+ name: 'IdeTreeList',
components: {
GlSkeletonLoading,
NavDropdown,
@@ -39,14 +35,6 @@ export default {
}
},
},
- beforeCreate() {
- performanceMarkAndMeasure({ mark: WEBIDE_MARK_TREE_START });
- },
- updated() {
- if (this.currentTree?.tree?.length) {
- eventHub.$emit(WEBIDE_MEASURE_TREE_FROM_REQUEST);
- }
- },
methods: {
...mapActions(['toggleTreeOpen']),
clickedFile() {
diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue
index 2307efd1d24..a2338c6dec5 100644
--- a/app/assets/javascripts/ide/components/nav_dropdown.vue
+++ b/app/assets/javascripts/ide/components/nav_dropdown.vue
@@ -45,7 +45,7 @@ export default {
</script>
<template>
- <div ref="dropdown" class="btn-group ide-nav-dropdown dropdown">
+ <div ref="dropdown" class="btn-group ide-nav-dropdown dropdown" data-testid="ide-nav-dropdown">
<nav-dropdown-button :show-merge-requests="canReadMergeRequests" />
<div class="dropdown-menu dropdown-menu-left p-0">
<nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" />
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index c8a825065f1..1f029612c29 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -6,9 +6,10 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import {
WEBIDE_MARK_FILE_CLICKED,
- WEBIDE_MARK_FILE_START,
+ WEBIDE_MARK_REPO_EDITOR_START,
+ WEBIDE_MARK_REPO_EDITOR_FINISH,
+ WEBIDE_MEASURE_REPO_EDITOR,
WEBIDE_MEASURE_FILE_AFTER_INTERACTION,
- WEBIDE_MEASURE_FILE_FROM_REQUEST,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import eventHub from '../eventhub';
@@ -28,6 +29,7 @@ import { getRulesWithTraversal } from '../lib/editorconfig/parser';
import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
export default {
+ name: 'RepoEditor',
components: {
ContentViewer,
DiffViewer,
@@ -175,9 +177,6 @@ export default {
}
},
},
- beforeCreate() {
- performanceMarkAndMeasure({ mark: WEBIDE_MARK_FILE_START });
- },
beforeDestroy() {
this.editor.dispose();
},
@@ -204,6 +203,7 @@ export default {
]),
...mapActions('editor', ['updateFileEditor']),
initEditor() {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_REPO_EDITOR_START });
if (this.shouldHideEditor && (this.file.content || this.file.raw)) {
return;
}
@@ -305,7 +305,15 @@ export default {
if (performance.getEntriesByName(WEBIDE_MARK_FILE_CLICKED).length) {
eventHub.$emit(WEBIDE_MEASURE_FILE_AFTER_INTERACTION);
} else {
- eventHub.$emit(WEBIDE_MEASURE_FILE_FROM_REQUEST);
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_REPO_EDITOR_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_REPO_EDITOR,
+ start: WEBIDE_MARK_REPO_EDITOR_START,
+ },
+ ],
+ });
}
},
refreshEditorDimensions() {
diff --git a/app/assets/javascripts/ide/components/terminal/session.vue b/app/assets/javascripts/ide/components/terminal/session.vue
index a8fe9ea6866..0e67a2ab45f 100644
--- a/app/assets/javascripts/ide/components/terminal/session.vue
+++ b/app/assets/javascripts/ide/components/terminal/session.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapState } from 'vuex';
+import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
import Terminal from './terminal.vue';
import { isEndingStatus } from '../../stores/modules/terminal/utils';
@@ -7,6 +8,7 @@ import { isEndingStatus } from '../../stores/modules/terminal/utils';
export default {
components: {
Terminal,
+ GlButton,
},
computed: {
...mapState('terminal', ['session']),
@@ -14,15 +16,17 @@ export default {
if (isEndingStatus(this.session.status)) {
return {
action: () => this.restartSession(),
+ variant: 'info',
+ category: 'primary',
text: __('Restart Terminal'),
- class: 'btn-primary',
};
}
return {
action: () => this.stopSession(),
+ variant: 'danger',
+ category: 'secondary',
text: __('Stop Terminal'),
- class: 'btn-inverted btn-remove',
};
},
},
@@ -37,15 +41,13 @@ export default {
<header class="ide-job-header d-flex align-items-center">
<h5>{{ __('Web Terminal') }}</h5>
<div class="ml-auto align-self-center">
- <button
+ <gl-button
v-if="actionButton"
- type="button"
- class="btn btn-sm"
- :class="actionButton.class"
+ :variant="actionButton.variant"
+ :category="actionButton.category"
@click="actionButton.action"
+ >{{ actionButton.text }}</gl-button
>
- {{ actionButton.text }}
- </button>
</div>
</header>
<terminal :terminal-path="session.terminalPath" :status="session.status" />
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 396aedbfa10..b9ebacef7e1 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -3,6 +3,12 @@ import IdeRouter from '~/ide/ide_router_extension';
import { joinPaths } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash';
import { __ } from '~/locale';
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import {
+ WEBIDE_MARK_FETCH_PROJECT_DATA_START,
+ WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
+ WEBIDE_MEASURE_FETCH_PROJECT_DATA,
+} from '~/performance/constants';
import { syncRouterAndStore } from './sync_router_and_store';
Vue.use(IdeRouter);
@@ -69,6 +75,7 @@ export const createRouter = store => {
router.beforeEach((to, from, next) => {
if (to.params.namespace && to.params.project) {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START });
store
.dispatch('getProjectData', {
namespace: to.params.namespace,
@@ -81,6 +88,15 @@ export const createRouter = store => {
const mergeRequestId = to.params.mrid;
if (branchId) {
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_FETCH_PROJECT_DATA,
+ start: WEBIDE_MARK_FETCH_PROJECT_DATA_START,
+ },
+ ],
+ });
store.dispatch('openBranch', {
projectId,
branchId,
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 56d48e87c18..62f49ba56b1 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import { mapActions } from 'vuex';
import { identity } from 'lodash';
import Translate from '~/vue_shared/translate';
+import PerformancePlugin from '~/performance/vue_performance_plugin';
import ide from './components/ide.vue';
import { createStore } from './stores';
import { createRouter } from './ide_router';
@@ -11,6 +12,10 @@ import { DEFAULT_THEME } from './lib/themes';
Vue.use(Translate);
+Vue.use(PerformancePlugin, {
+ components: ['FileTree'],
+});
+
/**
* Function that receives the default store and returns an extended one.
* @callback extendStoreCallback
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 1496170447d..710256b6377 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -3,6 +3,12 @@ import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import {
+ WEBIDE_MARK_FETCH_BRANCH_DATA_START,
+ WEBIDE_MARK_FETCH_BRANCH_DATA_FINISH,
+ WEBIDE_MEASURE_FETCH_BRANCH_DATA,
+} from '~/performance/constants';
import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys, commitActionTypes } from '../constants';
@@ -245,13 +251,23 @@ export const renameEntry = ({ dispatch, commit, state, getters }, { path, name,
dispatch('triggerFilesChange', { type: commitActionTypes.move, path, newPath });
};
-export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
- new Promise((resolve, reject) => {
+export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) => {
+ return new Promise((resolve, reject) => {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_BRANCH_DATA_START });
const currentProject = state.projects[projectId];
if (!currentProject || !currentProject.branches[branchId] || force) {
service
.getBranchData(projectId, branchId)
.then(({ data }) => {
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_FETCH_BRANCH_DATA_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_FETCH_BRANCH_DATA,
+ start: WEBIDE_MARK_FETCH_BRANCH_DATA_START,
+ },
+ ],
+ });
const { id } = data.commit;
commit(types.SET_BRANCH, {
projectPath: projectId,
@@ -291,6 +307,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
resolve(currentProject.branches[branchId]);
}
});
+};
export * from './actions/tree';
export * from './actions/file';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 4b9b958ddd6..8b43c7238fd 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -1,5 +1,11 @@
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import {
+ WEBIDE_MARK_FETCH_FILE_DATA_START,
+ WEBIDE_MARK_FETCH_FILE_DATA_FINISH,
+ WEBIDE_MEASURE_FETCH_FILE_DATA,
+} from '~/performance/constants';
import eventHub from '../../eventhub';
import service from '../../services';
import * as types from '../mutation_types';
@@ -61,6 +67,7 @@ export const getFileData = (
{ state, commit, dispatch, getters },
{ path, makeFileActive = true, openFile = makeFileActive, toggleLoading = true },
) => {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_FILE_DATA_START });
const file = state.entries[path];
const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path);
@@ -81,6 +88,15 @@ export const getFileData = (
return service
.getFileData(url)
.then(({ data }) => {
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_FETCH_FILE_DATA_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_FETCH_FILE_DATA,
+ start: WEBIDE_MARK_FETCH_FILE_DATA_START,
+ },
+ ],
+ });
if (data) commit(types.SET_FILE_DATA, { data, file });
if (openFile) commit(types.TOGGLE_FILE_OPEN, path);
@@ -150,6 +166,13 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) =
export const changeFileContent = ({ commit, state, getters }, { path, content }) => {
const file = state.entries[path];
+
+ // It's possible for monaco to hit a race condition where it tries to update renamed files.
+ // See issue https://gitlab.com/gitlab-org/gitlab/-/issues/284930
+ if (!file) {
+ return;
+ }
+
commit(types.UPDATE_FILE_CONTENT, {
path,
content,
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 3a7daf30cc4..23a5e26bc1c 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -1,4 +1,10 @@
import { defer } from 'lodash';
+import { performanceMarkAndMeasure } from '~/performance/utils';
+import {
+ WEBIDE_MARK_FETCH_FILES_FINISH,
+ WEBIDE_MEASURE_FETCH_FILES,
+ WEBIDE_MARK_FETCH_FILES_START,
+} from '~/performance/constants';
import { __ } from '../../../locale';
import service from '../../services';
import * as types from '../mutation_types';
@@ -46,8 +52,9 @@ export const setDirectoryData = ({ state, commit }, { projectId, branchId, treeL
});
};
-export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
- new Promise((resolve, reject) => {
+export const getFiles = ({ state, commit, dispatch }, payload = {}) => {
+ performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_FILES_START });
+ return new Promise((resolve, reject) => {
const { projectId, branchId, ref = branchId } = payload;
if (
@@ -61,6 +68,15 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
service
.getFiles(selectedProject.path_with_namespace, ref)
.then(({ data }) => {
+ performanceMarkAndMeasure({
+ mark: WEBIDE_MARK_FETCH_FILES_FINISH,
+ measures: [
+ {
+ name: WEBIDE_MEASURE_FETCH_FILES,
+ start: WEBIDE_MARK_FETCH_FILES_START,
+ },
+ ],
+ });
const { entries, treeList } = decorateFiles({ data });
commit(types.SET_ENTRIES, entries);
@@ -85,6 +101,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
resolve();
}
});
+};
export const restoreTree = ({ dispatch, commit, state }, path) => {
const entry = state.entries[path];
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 1d0814125e6..14d6f133d27 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -35,12 +35,14 @@ export default class IntegrationSettingsForm {
}
saveIntegration() {
- // Service was marked active so now we check;
+ // Save Service if not active and check the following if active;
// 1) If form contents are valid
// 2) If this service can be saved
// If both conditions are true, we override form submission
// and save the service using provided configuration.
- if (this.$form.get(0).checkValidity()) {
+ const formValid = this.$form.get(0).checkValidity() || this.formActive === false;
+
+ if (formValid) {
this.$form.submit();
} else {
eventHub.$emit('validateForm');
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
index e0fb58ef195..12f03873958 100644
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { loadCSSFile } from '../lib/utils/css_utils';
let instanceCount = 0;
@@ -13,10 +14,15 @@ class AutoWidthDropdownSelect {
const { dropdownClass } = this;
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- this.$selectElement.select2({
- dropdownCssClass: dropdownClass,
- ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
- });
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ this.$selectElement.select2({
+ dropdownCssClass: dropdownClass,
+ ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
+ });
+ })
+ .catch(() => {});
})
.catch(() => {});
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 6f2bd2da078..2072e41514d 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import Cookies from 'js-cookie';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import UsersSelect from './users_select';
+import { loadCSSFile } from './lib/utils/css_utils';
export default class IssuableContext {
constructor(currentUser) {
@@ -10,10 +11,15 @@ export default class IssuableContext {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+ })
+ .catch(() => {});
})
.catch(() => {});
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index ed34e2f5623..791b5fef699 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -7,6 +7,7 @@ import ZenMode from './zen_mode';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import { queryToObject, objectToQuery } from './lib/utils/url_utility';
+import { loadCSSFile } from './lib/utils/css_utils';
const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
const MR_TARGET_BRANCH = 'merge_request[target_branch]';
@@ -184,36 +185,41 @@ export default class IssuableForm {
initTargetBranchDropdown() {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- this.$targetBranchSelect.select2({
- ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
- ajax: {
- url: this.$targetBranchSelect.data('endpoint'),
- dataType: 'JSON',
- quietMillis: 250,
- data(search) {
- return {
- search,
- };
- },
- results(data) {
- return {
- // `data` keys are translated so we can't just access them with a string based key
- results: data[Object.keys(data)[0]].map(name => ({
- id: name,
- text: name,
- })),
- };
- },
- },
- initSelection(el, callback) {
- const val = el.val();
-
- callback({
- id: val,
- text: val,
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ this.$targetBranchSelect.select2({
+ ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
+ ajax: {
+ url: this.$targetBranchSelect.data('endpoint'),
+ dataType: 'JSON',
+ quietMillis: 250,
+ data(search) {
+ return {
+ search,
+ };
+ },
+ results(data) {
+ return {
+ // `data` keys are translated so we can't just access them with a string based key
+ results: data[Object.keys(data)[0]].map(name => ({
+ id: name,
+ text: name,
+ })),
+ };
+ },
+ },
+ initSelection(el, callback) {
+ const val = el.val();
+
+ callback({
+ id: val,
+ text: val,
+ });
+ },
});
- },
- });
+ })
+ .catch(() => {});
})
.catch(() => {});
}
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
index 1ee794ab208..583e5cb703d 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -128,7 +128,7 @@ export default {
<template>
<li class="issue gl-px-5!">
- <div class="issue-box">
+ <div class="issuable-info-container">
<div v-if="showCheckbox" class="issue-check">
<gl-form-checkbox
class="gl-mr-0"
@@ -136,101 +136,99 @@ export default {
@input="$emit('checked-input', $event)"
/>
</div>
- <div class="issuable-info-container">
- <div class="issuable-main-info">
- <div data-testid="issuable-title" class="issue-title title">
- <span class="issue-title-text" dir="auto">
- <gl-link :href="issuable.webUrl" v-bind="issuableTitleProps"
- >{{ issuable.title
- }}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2"
- /></gl-link>
- </span>
- </div>
- <div class="issuable-info">
- <slot v-if="hasSlotContents('reference')" name="reference"></slot>
- <span v-else data-testid="issuable-reference" class="issuable-reference"
- >{{ issuableSymbol }}{{ issuable.iid }}</span
- >
- <span class="issuable-authored d-none d-sm-inline-block">
- &middot;
- <span
- v-gl-tooltip:tooltipcontainer.bottom
- data-testid="issuable-created-at"
- :title="tooltipTitle(issuable.createdAt)"
- >{{ createdAt }}</span
- >
- {{ __('by') }}
- <slot v-if="hasSlotContents('author')" name="author"></slot>
- <gl-link
- v-else
- :data-user-id="authorId"
- :data-username="author.username"
- :data-name="author.name"
- :data-avatar-url="author.avatarUrl"
- :href="author.webUrl"
- data-testid="issuable-author"
- class="author-link js-user-link"
- >
- <span class="author">{{ author.name }}</span>
- </gl-link>
- </span>
- <slot name="timeframe"></slot>
- &nbsp;
- <gl-label
- v-for="(label, index) in labels"
- :key="index"
- :background-color="label.color"
- :title="labelTitle(label)"
- :description="label.description"
- :scoped="scopedLabel(label)"
- :target="labelTarget(label)"
- :class="{ 'gl-ml-2': index }"
- size="sm"
- />
- </div>
+ <div class="issuable-main-info">
+ <div data-testid="issuable-title" class="issue-title title">
+ <span class="issue-title-text" dir="auto">
+ <gl-link :href="issuable.webUrl" v-bind="issuableTitleProps"
+ >{{ issuable.title
+ }}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2"
+ /></gl-link>
+ </span>
</div>
- <div class="issuable-meta">
- <ul v-if="showIssuableMeta" class="controls">
- <li v-if="hasSlotContents('status')" class="issuable-status">
- <slot name="status"></slot>
- </li>
- <li
- v-if="showDiscussions"
- data-testid="issuable-discussions"
- class="issuable-comments gl-display-none gl-display-sm-block"
- >
- <gl-link
- v-gl-tooltip:tooltipcontainer.top
- :title="__('Comments')"
- :href="`${issuable.webUrl}#notes`"
- :class="{ 'no-comments': !issuable.userDiscussionsCount }"
- class="gl-reset-color!"
- >
- <gl-icon name="comments" />
- {{ issuable.userDiscussionsCount }}
- </gl-link>
- </li>
- <li v-if="assignees.length" class="gl-display-flex">
- <issuable-assignees
- :assignees="issuable.assignees"
- :icon-size="16"
- :max-visible="4"
- img-css-classes="gl-mr-2!"
- class="gl-align-items-center gl-display-flex gl-ml-3"
- />
- </li>
- </ul>
- <div
- data-testid="issuable-updated-at"
- class="float-right issuable-updated-at d-none d-sm-inline-block"
+ <div class="issuable-info">
+ <slot v-if="hasSlotContents('reference')" name="reference"></slot>
+ <span v-else data-testid="issuable-reference" class="issuable-reference"
+ >{{ issuableSymbol }}{{ issuable.iid }}</span
>
+ <span class="issuable-authored d-none d-sm-inline-block">
+ &middot;
<span
v-gl-tooltip:tooltipcontainer.bottom
- :title="tooltipTitle(issuable.updatedAt)"
- class="issuable-updated-at"
- >{{ updatedAt }}</span
+ data-testid="issuable-created-at"
+ :title="tooltipTitle(issuable.createdAt)"
+ >{{ createdAt }}</span
+ >
+ {{ __('by') }}
+ <slot v-if="hasSlotContents('author')" name="author"></slot>
+ <gl-link
+ v-else
+ :data-user-id="authorId"
+ :data-username="author.username"
+ :data-name="author.name"
+ :data-avatar-url="author.avatarUrl"
+ :href="author.webUrl"
+ data-testid="issuable-author"
+ class="author-link js-user-link"
+ >
+ <span class="author">{{ author.name }}</span>
+ </gl-link>
+ </span>
+ <slot name="timeframe"></slot>
+ &nbsp;
+ <gl-label
+ v-for="(label, index) in labels"
+ :key="index"
+ :background-color="label.color"
+ :title="labelTitle(label)"
+ :description="label.description"
+ :scoped="scopedLabel(label)"
+ :target="labelTarget(label)"
+ :class="{ 'gl-ml-2': index }"
+ size="sm"
+ />
+ </div>
+ </div>
+ <div class="issuable-meta">
+ <ul v-if="showIssuableMeta" class="controls">
+ <li v-if="hasSlotContents('status')" class="issuable-status">
+ <slot name="status"></slot>
+ </li>
+ <li
+ v-if="showDiscussions"
+ data-testid="issuable-discussions"
+ class="issuable-comments gl-display-none gl-display-sm-block"
+ >
+ <gl-link
+ v-gl-tooltip:tooltipcontainer.top
+ :title="__('Comments')"
+ :href="`${issuable.webUrl}#notes`"
+ :class="{ 'no-comments': !issuable.userDiscussionsCount }"
+ class="gl-reset-color!"
>
- </div>
+ <gl-icon name="comments" />
+ {{ issuable.userDiscussionsCount }}
+ </gl-link>
+ </li>
+ <li v-if="assignees.length" class="gl-display-flex">
+ <issuable-assignees
+ :assignees="issuable.assignees"
+ :icon-size="16"
+ :max-visible="4"
+ img-css-classes="gl-mr-2!"
+ class="gl-align-items-center gl-display-flex gl-ml-3"
+ />
+ </li>
+ </ul>
+ <div
+ data-testid="issuable-updated-at"
+ class="float-right issuable-updated-at d-none d-sm-inline-block"
+ >
+ <span
+ v-gl-tooltip:tooltipcontainer.bottom
+ :title="tooltipTitle(issuable.updatedAt)"
+ class="issuable-updated-at"
+ >{{ updatedAt }}</span
+ >
</div>
</div>
</div>
diff --git a/app/assets/javascripts/issue_show/utils/parse_data.js b/app/assets/javascripts/issue_show/utils/parse_data.js
index 620974901fb..aacbb6a9c6f 100644
--- a/app/assets/javascripts/issue_show/utils/parse_data.js
+++ b/app/assets/javascripts/issue_show/utils/parse_data.js
@@ -23,5 +23,3 @@ export const parseIssuableData = () => {
return {};
}
};
-
-export default {};
diff --git a/app/assets/javascripts/jira_connect/components/app.vue b/app/assets/javascripts/jira_connect/components/app.vue
index 6d32ba41eae..7b8b46cb048 100644
--- a/app/assets/javascripts/jira_connect/components/app.vue
+++ b/app/assets/javascripts/jira_connect/components/app.vue
@@ -1,7 +1,3 @@
-<script>
-export default {};
-</script>
-
<template>
<div></div>
</template>
diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/jobs/utils.js
index 28a125b2b8f..122f23a5bb5 100644
--- a/app/assets/javascripts/jobs/utils.js
+++ b/app/assets/javascripts/jobs/utils.js
@@ -1,4 +1,12 @@
-// capture anything starting with http:// or https://
-// up until a disallowed character or whitespace
-export const linkRegex = /(https?:\/\/[^"<>\\^`{|}\s]+)/g;
+/**
+ * capture anything starting with http:// or https://
+ * https?:\/\/
+ *
+ * up until a disallowed character or whitespace
+ * [^"<>\\^`{|}\s]+
+ *
+ * and a disallowed character or whitespace, including non-ending chars .,:;!?
+ * [^"<>\\^`{|}\s.,:;!?]
+ */
+export const linkRegex = /(https?:\/\/[^"<>\\^`{|}\s]+[^"<>\\^`{|}\s.,:;!?])/g;
export default { linkRegex };
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 42a5de68cfa..ef25fd83db9 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -218,23 +218,36 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
export const contentTop = () => {
- const perfBar = $('#js-peek').outerHeight() || 0;
- const mrTabsHeight = $('.merge-request-tabs').outerHeight() || 0;
- const headerHeight = $('.navbar-gitlab').outerHeight() || 0;
- const diffFilesChanged = $('.js-diff-files-changed').outerHeight() || 0;
- const isDesktop = breakpointInstance.isDesktop();
- const diffFileTitleBar =
- (isDesktop && $('.diff-file .file-title-flex-parent:visible').outerHeight()) || 0;
- const compareVersionsHeaderHeight = (isDesktop && $('.mr-version-controls').outerHeight()) || 0;
+ const heightCalculators = [
+ () => $('#js-peek').outerHeight(),
+ () => $('.navbar-gitlab').outerHeight(),
+ () => $('.merge-request-tabs').outerHeight(),
+ () => $('.js-diff-files-changed').outerHeight(),
+ () => {
+ const isDesktop = breakpointInstance.isDesktop();
+ const diffsTabIsActive = window.mrTabs?.currentAction === 'diffs';
+ let size;
+
+ if (isDesktop && diffsTabIsActive) {
+ size = $('.diff-file .file-title-flex-parent:visible').outerHeight();
+ }
- return (
- perfBar +
- mrTabsHeight +
- headerHeight +
- diffFilesChanged +
- diffFileTitleBar +
- compareVersionsHeaderHeight
- );
+ return size;
+ },
+ () => {
+ let size;
+
+ if (breakpointInstance.isDesktop()) {
+ size = $('.mr-version-controls').outerHeight();
+ }
+
+ return size;
+ },
+ ];
+
+ return heightCalculators.reduce((totalHeight, calculator) => {
+ return totalHeight + (calculator() || 0);
+ }, 0);
};
export const scrollToElement = (element, options = {}) => {
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index 7bba7ba2f45..2f19a0c9b26 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -1,6 +1,14 @@
import { has } from 'lodash';
import { isInIssuePage, isInMRPage, isInEpicPage } from './common_utils';
+/**
+ * Checks whether an element's content exceeds the element's width.
+ *
+ * @param element DOM element to check
+ */
+export const hasHorizontalOverflow = element =>
+ Boolean(element && element.scrollWidth > element.offsetWidth);
+
export const addClassIfElementExists = (element, className) => {
if (element) {
element.classList.add(className);
diff --git a/app/assets/javascripts/lib/utils/scroll_utils.js b/app/assets/javascripts/lib/utils/scroll_utils.js
index b4da1e16f08..01e43fd3b93 100644
--- a/app/assets/javascripts/lib/utils/scroll_utils.js
+++ b/app/assets/javascripts/lib/utils/scroll_utils.js
@@ -49,5 +49,3 @@ export const toggleDisableButton = ($button, disable) => {
if (disable && $button.prop('disabled')) return;
$button.prop('disabled', disable);
};
-
-export default {};
diff --git a/app/assets/javascripts/logs/utils.js b/app/assets/javascripts/logs/utils.js
index 8e537a4025f..880f762e225 100644
--- a/app/assets/javascripts/logs/utils.js
+++ b/app/assets/javascripts/logs/utils.js
@@ -23,5 +23,3 @@ export const getTimeRange = (seconds = 0) => {
};
export const formatDate = timestamp => dateFormat(timestamp, dateFormatMask);
-
-export default {};
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/access_request_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
index 10078d5cd64..10078d5cd64 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/access_request_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/action_button_group.vue b/app/assets/javascripts/members/components/action_buttons/action_button_group.vue
index 8356fdb60b1..8356fdb60b1 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/action_button_group.vue
+++ b/app/assets/javascripts/members/components/action_buttons/action_button_group.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/approve_access_request_button.vue b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
index e8a53ff173d..e8a53ff173d 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/approve_access_request_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/group_action_buttons.vue
index 2aebfe80db5..2aebfe80db5 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/group_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/group_action_buttons.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/invite_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
index 2b0a75640e2..2b0a75640e2 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/invite_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/leave_button.vue b/app/assets/javascripts/members/components/action_buttons/leave_button.vue
index d9976e7181c..443a962e0cf 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/leave_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/leave_button.vue
@@ -2,7 +2,7 @@
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import LeaveModal from '../modals/leave_modal.vue';
-import { LEAVE_MODAL_ID } from '../constants';
+import { LEAVE_MODAL_ID } from '../../constants';
export default {
name: 'LeaveButton',
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_group_link_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
index 9d89cb40676..9d89cb40676 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_group_link_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index b0b7ff4ce9a..b0b7ff4ce9a 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/resend_invite_button.vue b/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue
index 1cc3fd17e98..1cc3fd17e98 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/resend_invite_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/resend_invite_button.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
index 484dbb8fef5..f2bc9c7e876 100644
--- a/app/assets/javascripts/vue_shared/components/members/action_buttons/user_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
@@ -11,7 +11,7 @@ export default {
RemoveMemberButton,
LeaveButton,
LdapOverrideButton: () =>
- import('ee_component/vue_shared/components/members/ldap/ldap_override_button.vue'),
+ import('ee_component/members/components/ldap/ldap_override_button.vue'),
},
props: {
member: {
diff --git a/app/assets/javascripts/vue_shared/components/members/avatars/group_avatar.vue b/app/assets/javascripts/members/components/avatars/group_avatar.vue
index 12b748f9ab6..3b176bf2b43 100644
--- a/app/assets/javascripts/vue_shared/components/members/avatars/group_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/group_avatar.vue
@@ -1,6 +1,6 @@
<script>
import { GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
-import { AVATAR_SIZE } from '../constants';
+import { AVATAR_SIZE } from '../../constants';
export default {
name: 'GroupAvatar',
diff --git a/app/assets/javascripts/vue_shared/components/members/avatars/invite_avatar.vue b/app/assets/javascripts/members/components/avatars/invite_avatar.vue
index 28654a60860..08e702007bb 100644
--- a/app/assets/javascripts/vue_shared/components/members/avatars/invite_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/invite_avatar.vue
@@ -1,6 +1,6 @@
<script>
import { GlAvatarLabeled } from '@gitlab/ui';
-import { AVATAR_SIZE } from '../constants';
+import { AVATAR_SIZE } from '../../constants';
export default {
name: 'InviteAvatar',
diff --git a/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue b/app/assets/javascripts/members/components/avatars/user_avatar.vue
index e5e7cdf149c..fe45ca769af 100644
--- a/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/user_avatar.vue
@@ -5,9 +5,9 @@ import {
GlBadge,
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
-import { generateBadges } from 'ee_else_ce/vue_shared/components/members/utils';
+import { generateBadges } from 'ee_else_ce/members/utils';
import { __ } from '~/locale';
-import { AVATAR_SIZE } from '../constants';
+import { AVATAR_SIZE } from '../../constants';
import { glEmojiTag } from '~/emoji';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/members/modals/leave_modal.vue b/app/assets/javascripts/members/components/modals/leave_modal.vue
index 9a2ce0d4931..57a5da774e3 100644
--- a/app/assets/javascripts/vue_shared/components/members/modals/leave_modal.vue
+++ b/app/assets/javascripts/members/components/modals/leave_modal.vue
@@ -3,7 +3,7 @@ import { mapState } from 'vuex';
import { GlModal, GlForm, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
-import { LEAVE_MODAL_ID } from '../constants';
+import { LEAVE_MODAL_ID } from '../../constants';
export default {
name: 'LeaveModal',
diff --git a/app/assets/javascripts/vue_shared/components/members/modals/remove_group_link_modal.vue b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
index e8890717724..231d014a4ec 100644
--- a/app/assets/javascripts/vue_shared/components/members/modals/remove_group_link_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
@@ -3,7 +3,7 @@ import { mapState, mapActions } from 'vuex';
import { GlModal, GlSprintf, GlForm } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
-import { REMOVE_GROUP_LINK_MODAL_ID } from '../constants';
+import { REMOVE_GROUP_LINK_MODAL_ID } from '../../constants';
export default {
name: 'RemoveGroupLinkModal',
diff --git a/app/assets/javascripts/vue_shared/components/members/table/created_at.vue b/app/assets/javascripts/members/components/table/created_at.vue
index 0bad70894f9..0bad70894f9 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/created_at.vue
+++ b/app/assets/javascripts/members/components/table/created_at.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/table/expiration_datepicker.vue b/app/assets/javascripts/members/components/table/expiration_datepicker.vue
index 0a8af81c1d1..0a8af81c1d1 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/expiration_datepicker.vue
+++ b/app/assets/javascripts/members/components/table/expiration_datepicker.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/table/expires_at.vue b/app/assets/javascripts/members/components/table/expires_at.vue
index de65e3fb10f..c91de061b50 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/expires_at.vue
+++ b/app/assets/javascripts/members/components/table/expires_at.vue
@@ -6,7 +6,7 @@ import {
formatDate,
getDayDifference,
} from '~/lib/utils/datetime_utility';
-import { DAYS_TO_EXPIRE_SOON } from '../constants';
+import { DAYS_TO_EXPIRE_SOON } from '../../constants';
export default {
name: 'ExpiresAt',
diff --git a/app/assets/javascripts/vue_shared/components/members/table/member_action_buttons.vue b/app/assets/javascripts/members/components/table/member_action_buttons.vue
index 320d8c99223..c61ebec33bd 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/member_action_buttons.vue
+++ b/app/assets/javascripts/members/components/table/member_action_buttons.vue
@@ -3,7 +3,7 @@ import UserActionButtons from '../action_buttons/user_action_buttons.vue';
import GroupActionButtons from '../action_buttons/group_action_buttons.vue';
import InviteActionButtons from '../action_buttons/invite_action_buttons.vue';
import AccessRequestActionButtons from '../action_buttons/access_request_action_buttons.vue';
-import { MEMBER_TYPES } from '../constants';
+import { MEMBER_TYPES } from '../../constants';
export default {
name: 'MemberActionButtons',
diff --git a/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue b/app/assets/javascripts/members/components/table/member_avatar.vue
index a1f98d4008a..a1f98d4008a 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue
+++ b/app/assets/javascripts/members/components/table/member_avatar.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue
index 030d72c3420..030d72c3420 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/member_source.vue
+++ b/app/assets/javascripts/members/components/table/member_source.vue
diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index a4f67caff31..da77e5caad2 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -1,14 +1,9 @@
<script>
import { mapState } from 'vuex';
import { GlTable, GlBadge } from '@gitlab/ui';
-import MembersTableCell from 'ee_else_ce/vue_shared/components/members/table/members_table_cell.vue';
-import {
- canOverride,
- canRemove,
- canResend,
- canUpdate,
-} from 'ee_else_ce/vue_shared/components/members/utils';
-import { FIELDS } from '../constants';
+import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
+import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
+import { FIELDS } from '../../constants';
import initUserPopovers from '~/user_popovers';
import MemberAvatar from './member_avatar.vue';
import MemberSource from './member_source.vue';
@@ -34,9 +29,7 @@ export default {
RemoveGroupLinkModal,
ExpirationDatepicker,
LdapOverrideConfirmationModal: () =>
- import(
- 'ee_component/vue_shared/components/members/ldap/ldap_override_confirmation_modal.vue'
- ),
+ import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
},
computed: {
...mapState(['members', 'tableFields', 'tableAttrs', 'currentUserId', 'sourceId']),
diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue
index 11e1aef9803..20aa01b96bc 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue
+++ b/app/assets/javascripts/members/components/table/members_table_cell.vue
@@ -1,7 +1,14 @@
<script>
import { mapState } from 'vuex';
-import { MEMBER_TYPES } from '../constants';
-import { isGroup, isDirectMember, isCurrentUser, canRemove, canResend, canUpdate } from '../utils';
+import { MEMBER_TYPES } from '../../constants';
+import {
+ isGroup,
+ isDirectMember,
+ isCurrentUser,
+ canRemove,
+ canResend,
+ canUpdate,
+} from '../../utils';
export default {
name: 'MembersTableCell',
diff --git a/app/assets/javascripts/vue_shared/components/members/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index 6f6cae6072d..8ad45ab6920 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -9,8 +9,7 @@ export default {
components: {
GlDropdown,
GlDropdownItem,
- LdapDropdownItem: () =>
- import('ee_component/vue_shared/components/members/ldap/ldap_dropdown_item.vue'),
+ LdapDropdownItem: () => import('ee_component/members/components/ldap/ldap_dropdown_item.vue'),
},
props: {
member: {
diff --git a/app/assets/javascripts/vue_shared/components/members/constants.js b/app/assets/javascripts/members/constants.js
index 5885420a122..5885420a122 100644
--- a/app/assets/javascripts/vue_shared/components/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
diff --git a/app/assets/javascripts/vuex_shared/modules/members/actions.js b/app/assets/javascripts/members/store/actions.js
index 4c31b3c9744..4c31b3c9744 100644
--- a/app/assets/javascripts/vuex_shared/modules/members/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
diff --git a/app/assets/javascripts/members/store/index.js b/app/assets/javascripts/members/store/index.js
new file mode 100644
index 00000000000..f219f8931b0
--- /dev/null
+++ b/app/assets/javascripts/members/store/index.js
@@ -0,0 +1,9 @@
+import createState from 'ee_else_ce/members/store/state';
+import mutations from 'ee_else_ce/members/store/mutations';
+import * as actions from 'ee_else_ce/members/store/actions';
+
+export default initialState => ({
+ state: createState(initialState),
+ actions,
+ mutations,
+});
diff --git a/app/assets/javascripts/vuex_shared/modules/members/mutation_types.js b/app/assets/javascripts/members/store/mutation_types.js
index 77307aa745b..77307aa745b 100644
--- a/app/assets/javascripts/vuex_shared/modules/members/mutation_types.js
+++ b/app/assets/javascripts/members/store/mutation_types.js
diff --git a/app/assets/javascripts/vuex_shared/modules/members/mutations.js b/app/assets/javascripts/members/store/mutations.js
index 2415e744290..2415e744290 100644
--- a/app/assets/javascripts/vuex_shared/modules/members/mutations.js
+++ b/app/assets/javascripts/members/store/mutations.js
diff --git a/app/assets/javascripts/vuex_shared/modules/members/state.js b/app/assets/javascripts/members/store/state.js
index ab3ebb34616..ab3ebb34616 100644
--- a/app/assets/javascripts/vuex_shared/modules/members/state.js
+++ b/app/assets/javascripts/members/store/state.js
diff --git a/app/assets/javascripts/vuex_shared/modules/members/utils.js b/app/assets/javascripts/members/store/utils.js
index 7dcd33111e8..7dcd33111e8 100644
--- a/app/assets/javascripts/vuex_shared/modules/members/utils.js
+++ b/app/assets/javascripts/members/store/utils.js
diff --git a/app/assets/javascripts/vue_shared/components/members/utils.js b/app/assets/javascripts/members/utils.js
index 4229a62c0a7..4229a62c0a7 100644
--- a/app/assets/javascripts/vue_shared/components/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index 25c357b6073..c803774f4a7 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -54,7 +54,6 @@ import { s__ } from '~/locale';
file.promptDiscardConfirmation = false;
file.resolveMode = DEFAULT_RESOLVE_MODE;
file.filePath = this.getFilePath(file);
- file.iconClass = `fa-${file.blob_icon}`;
file.blobPath = file.blob_path;
if (file.type === CONFLICT_TYPES.TEXT) {
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index a5a930572e1..229f6f3e339 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
import { deprecatedCreateFlash as createFlash } from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
@@ -24,6 +25,7 @@ export default function initMergeConflicts() {
gl.MergeConflictsResolverApp = new Vue({
el: '#conflicts',
components: {
+ FileIcon,
'diff-file-editor': gl.mergeConflicts.diffFileEditor,
'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines,
'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines,
diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js
index 9245ffdb3b9..4ae5cf04ff9 100644
--- a/app/assets/javascripts/monitoring/stores/variable_mapping.js
+++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js
@@ -271,5 +271,3 @@ export const optionsFromSeriesData = ({ label, data = [] }) => {
return [...optionsSet].map(parseSimpleCustomValues);
};
-
-export default {};
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 92bbce498d5..a4c5a881fae 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -404,5 +404,3 @@ export const barChartsDataParser = (data = []) =>
}),
{},
);
-
-export default {};
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 9be53fe60f2..5073922e4a4 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -23,6 +23,7 @@ import {
commentLineOptions,
formatLineRange,
} from './multiline_comment_utils';
+import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
export default {
name: 'NoteableNote',
@@ -169,12 +170,8 @@ export default {
return this.line && this.startLineNumber !== this.endLineNumber;
},
commentLineOptions() {
- const sideA = this.line.type === 'new' ? 'right' : 'left';
- const sideB = sideA === 'left' ? 'right' : 'left';
- const lines = this.diffFile.highlighted_diff_lines.length
- ? this.diffFile.highlighted_diff_lines
- : this.diffFile.parallel_diff_lines.map(l => l[sideA] || l[sideB]);
- return commentLineOptions(lines, this.commentLineStart, this.line.line_code, sideA);
+ const lines = this.diffFile[INLINE_DIFF_LINES_KEY].length;
+ return commentLineOptions(lines, this.commentLineStart, this.line.line_code);
},
diffFile() {
if (this.commentLineStart.line_code) {
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index 61298a15c5d..c6932bfacae 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,16 +1,17 @@
import { mapGetters, mapActions, mapState } from 'vuex';
-import { scrollToElementWithContext } from '~/lib/utils/common_utils';
+import { scrollToElementWithContext, scrollToElement } from '~/lib/utils/common_utils';
import eventHub from '../event_hub';
/**
* @param {string} selector
* @returns {boolean}
*/
-function scrollTo(selector) {
+function scrollTo(selector, { withoutContext = false } = {}) {
const el = document.querySelector(selector);
+ const scrollFunction = withoutContext ? scrollToElement : scrollToElementWithContext;
if (el) {
- scrollToElementWithContext(el);
+ scrollFunction(el);
return true;
}
@@ -35,7 +36,7 @@ function diffsJump({ expandDiscussion }, id) {
function discussionJump({ expandDiscussion }, id) {
const selector = `div.discussion[data-discussion-id="${id}"]`;
expandDiscussion({ discussionId: id });
- return scrollTo(selector);
+ return scrollTo(selector, { withoutContext: true });
}
/**
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 2c60b5ee84a..ee668f4406f 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -435,6 +435,10 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
};
const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
+ if (state.isResolvingDiscussion) {
+ return null;
+ }
+
if (resp.notes?.length) {
dispatch('updateOrCreateNotes', resp.notes);
dispatch('startTaskList');
@@ -574,6 +578,9 @@ export const submitSuggestion = (
const dispatchResolveDiscussion = () =>
dispatch('resolveDiscussion', { discussionId }).catch(() => {});
+ commit(types.SET_RESOLVING_DISCUSSION, true);
+ dispatch('stopPolling');
+
return Api.applySuggestion(suggestionId)
.then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }))
.then(dispatchResolveDiscussion)
@@ -587,6 +594,10 @@ export const submitSuggestion = (
const flashMessage = errorMessage || defaultMessage;
Flash(__(flashMessage), 'alert', flashContainer);
+ })
+ .finally(() => {
+ commit(types.SET_RESOLVING_DISCUSSION, false);
+ dispatch('restartPolling');
});
};
@@ -605,6 +616,8 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai
});
commit(types.SET_APPLYING_BATCH_STATE, true);
+ commit(types.SET_RESOLVING_DISCUSSION, true);
+ dispatch('stopPolling');
return Api.applySuggestionBatch(suggestionIds)
.then(() => Promise.all(applyAllSuggestions()))
@@ -621,7 +634,11 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai
Flash(__(flashMessage), 'alert', flashContainer);
})
- .finally(() => commit(types.SET_APPLYING_BATCH_STATE, false));
+ .finally(() => {
+ commit(types.SET_APPLYING_BATCH_STATE, false);
+ commit(types.SET_RESOLVING_DISCUSSION, false);
+ dispatch('restartPolling');
+ });
};
export const addSuggestionInfoToBatch = ({ commit }, { suggestionId, noteId, discussionId }) =>
diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js
index d94fc626a3f..f34247d4eb0 100644
--- a/app/assets/javascripts/notes/stores/collapse_utils.js
+++ b/app/assets/javascripts/notes/stores/collapse_utils.js
@@ -70,6 +70,3 @@ export const collapseSystemNotes = notes => {
return acc;
}, []);
};
-
-// for babel-rewire
-export default {};
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index a8738fa7c5f..3194a2099ea 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -42,6 +42,7 @@ export default () => ({
current_user: {},
preview_note_path: 'path/to/preview',
},
+ isResolvingDiscussion: false,
commentsDisabled: false,
resolvableDiscussionsCount: 0,
unresolvedDiscussionsCount: 0,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 7496dd630f6..8270f2a225b 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -38,6 +38,7 @@ export const SET_TIMELINE_VIEW = 'SET_TIMELINE_VIEW';
export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION';
export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER';
export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS';
+export const SET_RESOLVING_DISCUSSION = 'SET_RESOLVING_DISCUSSION';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 7cc619ec1c5..85bdf60e8f9 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -213,6 +213,10 @@ export default {
}
},
+ [types.SET_RESOLVING_DISCUSSION](state, isResolving) {
+ state.isResolvingDiscussion = isResolving;
+ },
+
[types.UPDATE_NOTE](state, note) {
const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id);
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
index 6a0e92bff2d..e14696e0d1c 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -68,6 +68,10 @@ export const PACKAGE_REGISTRY_TABS = [
title: s__('PackageRegistry|Conan'),
type: PackageType.CONAN,
},
+ {
+ title: s__('PackageRegistry|Generic'),
+ type: PackageType.GENERIC,
+ },
{
title: s__('PackageRegistry|Maven'),
diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js
index c481abd8658..c0f7f150337 100644
--- a/app/assets/javascripts/packages/shared/constants.js
+++ b/app/assets/javascripts/packages/shared/constants.js
@@ -7,6 +7,7 @@ export const PackageType = {
NUGET: 'nuget',
PYPI: 'pypi',
COMPOSER: 'composer',
+ GENERIC: 'generic',
};
export const TrackingActions = {
diff --git a/app/assets/javascripts/packages/shared/utils.js b/app/assets/javascripts/packages/shared/utils.js
index b0807558266..d7a883e4397 100644
--- a/app/assets/javascripts/packages/shared/utils.js
+++ b/app/assets/javascripts/packages/shared/utils.js
@@ -21,7 +21,8 @@ export const getPackageTypeLabel = packageType => {
return s__('PackageType|PyPI');
case PackageType.COMPOSER:
return s__('PackageType|Composer');
-
+ case PackageType.GENERIC:
+ return s__('PackageType|Generic');
default:
return null;
}
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 1879e263ce7..74c1c2e981e 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -73,22 +73,20 @@ document.addEventListener('DOMContentLoaded', () => {
);
}
- if (gon.features?.suggestPipeline) {
- const successPipelineEl = document.querySelector('.js-success-pipeline-modal');
+ const successPipelineEl = document.querySelector('.js-success-pipeline-modal');
- if (successPipelineEl) {
- // eslint-disable-next-line no-new
- new Vue({
- el: successPipelineEl,
- render(createElement) {
- return createElement(PipelineTourSuccessModal, {
- props: {
- ...successPipelineEl.dataset,
- },
- });
- },
- });
- }
+ if (successPipelineEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: successPipelineEl,
+ render(createElement) {
+ return createElement(PipelineTourSuccessModal, {
+ props: {
+ ...successPipelineEl.dataset,
+ },
+ });
+ },
+ });
}
if (gon?.features?.gitlabCiYmlPreview) {
diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
index 26dea17ca8a..eaf340f2725 100644
--- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js
+++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
@@ -1,8 +1,5 @@
import { initCommitBoxInfo } from '~/projects/commit_box/info';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
-document.addEventListener('DOMContentLoaded', () => {
- initCommitBoxInfo();
-
- initPipelines();
-});
+initCommitBoxInfo();
+initPipelines();
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index e0bd49bf6ef..0750f472341 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -15,35 +15,33 @@ import { __ } from '~/locale';
import loadAwardsHandler from '~/awards_handler';
import { initCommitBoxInfo } from '~/projects/commit_box/info';
-document.addEventListener('DOMContentLoaded', () => {
- const hasPerfBar = document.querySelector('.with-performance-bar');
- const performanceHeight = hasPerfBar ? 35 : 0;
- initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
- new ZenMode();
- new ShortcutsNavigation();
+const hasPerfBar = document.querySelector('.with-performance-bar');
+const performanceHeight = hasPerfBar ? 35 : 0;
+initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
+new ZenMode();
+new ShortcutsNavigation();
- initCommitBoxInfo();
+initCommitBoxInfo();
- initNotes();
+initNotes();
- const filesContainer = $('.js-diffs-batch');
+const filesContainer = $('.js-diffs-batch');
- if (filesContainer.length) {
- const batchPath = filesContainer.data('diffFilesPath');
+if (filesContainer.length) {
+ const batchPath = filesContainer.data('diffFilesPath');
- axios
- .get(batchPath)
- .then(({ data }) => {
- filesContainer.html($(data.html));
- syntaxHighlight(filesContainer);
- handleLocationHash();
- new Diff();
- })
- .catch(() => {
- flash({ message: __('An error occurred while retrieving diff files') });
- });
- } else {
- new Diff();
- }
- loadAwardsHandler();
-});
+ axios
+ .get(batchPath)
+ .then(({ data }) => {
+ filesContainer.html($(data.html));
+ syntaxHighlight(filesContainer);
+ handleLocationHash();
+ new Diff();
+ })
+ .catch(() => {
+ flash({ message: __('An error occurred while retrieving diff files') });
+ });
+} else {
+ new Diff();
+}
+loadAwardsHandler();
diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js
index b456baac612..6239e4c99d2 100644
--- a/app/assets/javascripts/pages/projects/commits/show/index.js
+++ b/app/assets/javascripts/pages/projects/commits/show/index.js
@@ -1,12 +1,9 @@
import CommitsList from '~/commits';
import GpgBadges from '~/gpg_badges';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-
import mountCommits from '~/projects/commits';
-document.addEventListener('DOMContentLoaded', () => {
- new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
- new ShortcutsNavigation(); // eslint-disable-line no-new
- GpgBadges.fetch();
- mountCommits(document.getElementById('js-author-dropdown'));
-});
+new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
+new ShortcutsNavigation(); // eslint-disable-line no-new
+GpgBadges.fetch();
+mountCommits(document.getElementById('js-author-dropdown'));
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index 477a1ab887b..19aeb1d1ecf 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -2,46 +2,28 @@ import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash';
-import Tracking from '~/tracking';
-import { isExperimentEnabled } from '~/lib/utils/experimentation';
document.addEventListener('DOMContentLoaded', () => {
initProjectVisibilitySelector();
initProjectNew.bindEvents();
- const { category, property } = gon.tracking_data ?? { category: 'projects:new' };
- const hasNewCreateProjectUi = isExperimentEnabled('newCreateProjectUi');
+ import(
+ /* webpackChunkName: 'experiment_new_project_creation' */ '../../../projects/experiment_new_project_creation'
+ )
+ .then(m => {
+ const el = document.querySelector('.js-experiment-new-project-creation');
- if (!hasNewCreateProjectUi) {
- // Setting additional tracking for HAML template
+ if (!el) {
+ return;
+ }
- Array.from(
- document.querySelectorAll('.project-edit-container [data-experiment-track-label]'),
- ).forEach(node =>
- node.addEventListener('click', event => {
- const { experimentTrackLabel: label } = event.currentTarget.dataset;
- Tracking.event(category, 'click_tab', { property, label });
- }),
- );
- } else {
- import(
- /* webpackChunkName: 'experiment_new_project_creation' */ '../../../projects/experiment_new_project_creation'
- )
- .then(m => {
- const el = document.querySelector('.js-experiment-new-project-creation');
-
- if (!el) {
- return;
- }
-
- const config = {
- hasErrors: 'hasErrors' in el.dataset,
- isCiCdAvailable: 'isCiCdAvailable' in el.dataset,
- };
- m.default(el, config);
- })
- .catch(() => {
- createFlash(__('An error occurred while loading project creation UI'));
- });
- }
+ const config = {
+ hasErrors: 'hasErrors' in el.dataset,
+ isCiCdAvailable: 'isCiCdAvailable' in el.dataset,
+ };
+ m.default(el, config);
+ })
+ .catch(() => {
+ createFlash(__('An error occurred while loading project creation UI'));
+ });
});
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 5317093c4cf..8c7aa04a0b6 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -9,47 +9,11 @@ import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
import projectSelect from '../../project_select';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import initClonePanel from '~/clone_panel';
export default class Project {
constructor() {
- const $cloneOptions = $('ul.clone-options-dropdown');
- if ($cloneOptions.length) {
- const $projectCloneField = $('#project_clone');
- const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
- const mobileCloneField = document.querySelector(
- '.js-mobile-git-clone .js-clone-dropdown-label',
- );
-
- const selectedCloneOption = $cloneBtnLabel.text().trim();
- if (selectedCloneOption.length > 0) {
- $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
- }
-
- $('a', $cloneOptions).on('click', e => {
- e.preventDefault();
- const $this = $(e.currentTarget);
- const url = $this.attr('href');
- const cloneType = $this.data('cloneType');
-
- $('.is-active', $cloneOptions).removeClass('is-active');
- $(`a[data-clone-type="${cloneType}"]`).each(function() {
- const $el = $(this);
- const activeText = $el.find('.dropdown-menu-inner-title').text();
- const $container = $el.closest('.project-clone-holder');
- const $label = $container.find('.js-clone-dropdown-label');
-
- $el.toggleClass('is-active');
- $label.text(activeText);
- });
-
- if (mobileCloneField) {
- mobileCloneField.dataset.clipboardText = url;
- } else {
- $projectCloneField.val(url);
- }
- $('.js-git-empty .js-clone').text(url);
- });
- }
+ initClonePanel();
// Ref switcher
if (document.querySelector('.js-project-refs-dropdown')) {
diff --git a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
index ae2209b0292..22dddb72f98 100644
--- a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
+++ b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
@@ -1,3 +1,3 @@
import initExpiresAtField from '~/access_tokens';
-document.addEventListener('DOMContentLoaded', initExpiresAtField);
+initExpiresAtField();
diff --git a/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
index ffc84dc106b..1dc238b56b4 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/create_deploy_token/index.js
@@ -1,3 +1,3 @@
import initForm from '../form';
-document.addEventListener('DOMContentLoaded', initForm);
+initForm();
diff --git a/app/assets/javascripts/performance/constants.js b/app/assets/javascripts/performance/constants.js
index 816eb9b3a66..069f3c265f3 100644
--- a/app/assets/javascripts/performance/constants.js
+++ b/app/assets/javascripts/performance/constants.js
@@ -19,16 +19,27 @@ export const SNIPPET_MEASURE_BLOBS_CONTENT = 'snippet-blobs-content';
// Marks
export const WEBIDE_MARK_APP_START = 'webide-app-start';
-export const WEBIDE_MARK_TREE_START = 'webide-tree-start';
-export const WEBIDE_MARK_TREE_FINISH = 'webide-tree-finished';
-export const WEBIDE_MARK_FILE_START = 'webide-file-start';
export const WEBIDE_MARK_FILE_CLICKED = 'webide-file-clicked';
export const WEBIDE_MARK_FILE_FINISH = 'webide-file-finished';
+export const WEBIDE_MARK_REPO_EDITOR_START = 'webide-init-editor-start';
+export const WEBIDE_MARK_REPO_EDITOR_FINISH = 'webide-init-editor-finish';
+export const WEBIDE_MARK_FETCH_BRANCH_DATA_START = 'webide-getBranchData-start';
+export const WEBIDE_MARK_FETCH_BRANCH_DATA_FINISH = 'webide-getBranchData-finish';
+export const WEBIDE_MARK_FETCH_FILE_DATA_START = 'webide-getFileData-start';
+export const WEBIDE_MARK_FETCH_FILE_DATA_FINISH = 'webide-getFileData-finish';
+export const WEBIDE_MARK_FETCH_FILES_START = 'webide-getFiles-start';
+export const WEBIDE_MARK_FETCH_FILES_FINISH = 'webide-getFiles-finish';
+export const WEBIDE_MARK_FETCH_PROJECT_DATA_START = 'webide-getProjectData-start';
+export const WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH = 'webide-getProjectData-finish';
// Measures
-export const WEBIDE_MEASURE_TREE_FROM_REQUEST = 'webide-tree-loading-from-request';
-export const WEBIDE_MEASURE_FILE_FROM_REQUEST = 'webide-file-loading-from-request';
export const WEBIDE_MEASURE_FILE_AFTER_INTERACTION = 'webide-file-loading-after-interaction';
+export const WEBIDE_MEASURE_FETCH_PROJECT_DATA = 'WebIDE: Project data';
+export const WEBIDE_MEASURE_FETCH_BRANCH_DATA = 'WebIDE: Branch data';
+export const WEBIDE_MEASURE_FETCH_FILE_DATA = 'WebIDE: File data';
+export const WEBIDE_MEASURE_BEFORE_VUE = 'WebIDE: Before Vue app';
+export const WEBIDE_MEASURE_REPO_EDITOR = 'WebIDE: Repo Editor';
+export const WEBIDE_MEASURE_FETCH_FILES = 'WebIDE: Fetch Files';
//
// MR Diffs namespace
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index 8c5f45e9d34..d4857a19ff7 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -7,6 +7,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-buy-pipeline-minutes-notification-callout',
'.js-token-expiry-callout',
'.js-registration-enabled-callout',
+ '.js-new-user-signups-cap-reached',
];
const initCallouts = () => {
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
new file mode 100644
index 00000000000..9279273283e
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
@@ -0,0 +1,139 @@
+<script>
+import {
+ GlButton,
+ GlForm,
+ GlFormCheckbox,
+ GlFormInput,
+ GlFormGroup,
+ GlFormTextarea,
+ GlSprintf,
+} from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlForm,
+ GlFormCheckbox,
+ GlFormInput,
+ GlFormGroup,
+ GlFormTextarea,
+ GlSprintf,
+ },
+ props: {
+ defaultBranch: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ defaultMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isSaving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ message: this.defaultMessage,
+ branch: this.defaultBranch,
+ openMergeRequest: false,
+ };
+ },
+ computed: {
+ isDefaultBranch() {
+ return this.branch === this.defaultBranch;
+ },
+ submitDisabled() {
+ return !(this.message && this.branch);
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$emit('submit', {
+ message: this.message,
+ branch: this.branch,
+ openMergeRequest: this.openMergeRequest,
+ });
+ },
+ onReset() {
+ this.$emit('cancel');
+ },
+ },
+ i18n: {
+ commitMessage: __('Commit message'),
+ targetBranch: __('Target Branch'),
+ startMergeRequest: __('Start a %{new_merge_request} with these changes'),
+ newMergeRequest: __('new merge request'),
+ commitChanges: __('Commit changes'),
+ cancel: __('Cancel'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-form @submit.prevent="onSubmit" @reset.prevent="onReset">
+ <gl-form-group
+ id="commit-group"
+ :label="$options.i18n.commitMessage"
+ label-cols-sm="2"
+ label-for="commit-message"
+ >
+ <gl-form-textarea
+ id="commit-message"
+ v-model="message"
+ class="gl-font-monospace!"
+ required
+ :placeholder="defaultMessage"
+ />
+ </gl-form-group>
+ <gl-form-group
+ id="target-branch-group"
+ :label="$options.i18n.targetBranch"
+ label-cols-sm="2"
+ label-for="target-branch-field"
+ >
+ <gl-form-input
+ id="target-branch-field"
+ v-model="branch"
+ class="gl-font-monospace!"
+ required
+ />
+ <gl-form-checkbox
+ v-if="!isDefaultBranch"
+ v-model="openMergeRequest"
+ data-testid="new-mr-checkbox"
+ class="gl-mt-3"
+ >
+ <gl-sprintf :message="$options.i18n.startMergeRequest">
+ <template #new_merge_request>
+ <strong>{{ $options.i18n.newMergeRequest }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-form-checkbox>
+ </gl-form-group>
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-p-5 gl-bg-gray-10 gl-border-t-gray-100 gl-border-t-solid gl-border-t-1"
+ >
+ <gl-button
+ type="submit"
+ class="js-no-auto-disable"
+ category="primary"
+ variant="success"
+ :disabled="submitDisabled"
+ :loading="isSaving"
+ >
+ {{ $options.i18n.commitChanges }}
+ </gl-button>
+ <gl-button type="reset" category="secondary" class="gl-mr-3">
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/text_editor.vue
index a925077c906..22f2a32c9ac 100644
--- a/app/assets/javascripts/pipeline_editor/components/text_editor.vue
+++ b/app/assets/javascripts/pipeline_editor/components/text_editor.vue
@@ -5,22 +5,10 @@ export default {
components: {
EditorLite,
},
- props: {
- value: {
- type: String,
- required: false,
- default: '',
- },
- },
};
</script>
<template>
<div class="gl-border-solid gl-border-gray-100 gl-border-1">
- <editor-lite
- v-model="value"
- file-name="*.yml"
- :editor-options="{ readOnly: true }"
- @editor-ready="$emit('editor-ready')"
- />
+ <editor-lite file-name="*.yml" v-bind="$attrs" v-on="$listeners" />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
new file mode 100644
index 00000000000..11bca42fd69
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
@@ -0,0 +1,26 @@
+mutation commitCIFileMutation(
+ $projectPath: ID!
+ $branch: String!
+ $startBranch: String
+ $message: String!
+ $filePath: String!
+ $lastCommitId: String!
+ $content: String
+) {
+ commitCreate(
+ input: {
+ projectPath: $projectPath
+ branch: $branch
+ startBranch: $startBranch
+ message: $message
+ actions: [
+ { action: UPDATE, filePath: $filePath, lastCommitId: $lastCommitId, content: $content }
+ ]
+ }
+ ) {
+ commit {
+ id
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index ccd7b74064f..8268a907a29 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -10,7 +10,11 @@ import PipelineEditorApp from './pipeline_editor_app.vue';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const el = document.querySelector(selector);
- const { projectPath, defaultBranch, ciConfigPath } = el?.dataset;
+ if (!el) {
+ return null;
+ }
+
+ const { ciConfigPath, commitId, defaultBranch, newMergeRequestPath, projectPath } = el?.dataset;
Vue.use(VueApollo);
@@ -24,9 +28,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
render(h) {
return h(PipelineEditorApp, {
props: {
- projectPath,
- defaultBranch,
ciConfigPath,
+ commitId,
+ defaultBranch,
+ newMergeRequestPath,
+ projectPath,
},
});
},
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 50b946af456..59635296de4 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -1,20 +1,33 @@
<script>
-import { GlLoadingIcon, GlAlert, GlTabs, GlTab } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
+import { redirectTo, mergeUrlParams, refreshCurrentPage } from '~/lib/utils/url_utility';
-import TextEditor from './components/text_editor.vue';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import CommitForm from './components/commit/commit_form.vue';
+import TextEditor from './components/text_editor.vue';
+import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
+const MR_SOURCE_BRANCH = 'merge_request[source_branch]';
+const MR_TARGET_BRANCH = 'merge_request[target_branch]';
+
+const LOAD_FAILURE_NO_REF = 'LOAD_FAILURE_NO_REF';
+const LOAD_FAILURE_NO_FILE = 'LOAD_FAILURE_NO_FILE';
+const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
+const COMMIT_FAILURE = 'COMMIT_FAILURE';
+const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
+
export default {
components: {
- GlLoadingIcon,
GlAlert,
- GlTabs,
+ GlLoadingIcon,
GlTab,
- TextEditor,
+ GlTabs,
PipelineGraph,
+ CommitForm,
+ TextEditor,
},
props: {
projectPath: {
@@ -26,16 +39,30 @@ export default {
required: false,
default: null,
},
+ commitId: {
+ type: String,
+ required: false,
+ default: null,
+ },
ciConfigPath: {
type: String,
required: true,
},
+ newMergeRequestPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
- error: null,
- content: '',
+ showFailureAlert: false,
+ failureType: null,
+ failureReasons: [],
+
+ isSaving: false,
editorIsReady: false,
+ content: '',
+ contentModel: '',
};
},
apollo: {
@@ -51,51 +78,168 @@ export default {
update(data) {
return data?.blobContent?.rawData;
},
+ result({ data }) {
+ this.contentModel = data?.blobContent?.rawData ?? '';
+ },
error(error) {
- this.error = error;
+ this.handleBlobContentError(error);
},
},
},
computed: {
- loading() {
+ isLoading() {
return this.$apollo.queries.content.loading;
},
- errorMessage() {
- const { message: generalReason, networkError } = this.error ?? {};
-
- const { data } = networkError?.response ?? {};
- // 404 for missing file uses `message`
- // 400 for a missing ref uses `error`
- const networkReason = data?.message ?? data?.error;
-
- const reason = networkReason ?? generalReason ?? this.$options.i18n.unknownError;
- return sprintf(this.$options.i18n.errorMessageWithReason, { reason });
+ defaultCommitMessage() {
+ return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
},
pipelineData() {
// Note data will loaded as part of https://gitlab.com/gitlab-org/gitlab/-/issues/263141
return {};
},
+ failure() {
+ switch (this.failureType) {
+ case LOAD_FAILURE_NO_REF:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE_NO_REF],
+ variant: 'danger',
+ };
+ case LOAD_FAILURE_NO_FILE:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE_NO_FILE],
+ variant: 'danger',
+ };
+ case LOAD_FAILURE_UNKNOWN:
+ return {
+ text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
+ variant: 'danger',
+ };
+ case COMMIT_FAILURE:
+ return {
+ text: this.$options.errorTexts[COMMIT_FAILURE],
+ variant: 'danger',
+ };
+ default:
+ return {
+ text: this.$options.errorTexts[DEFAULT_FAILURE],
+ variant: 'danger',
+ };
+ }
+ },
},
i18n: {
- unknownError: __('Unknown Error'),
- errorMessageWithReason: s__('Pipelines|CI file could not be loaded: %{reason}'),
+ defaultCommitMessage: __('Update %{sourcePath} file'),
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
},
+ errorTexts: {
+ [LOAD_FAILURE_NO_REF]: s__(
+ 'Pipelines|Repository does not have a default branch, please set one.',
+ ),
+ [LOAD_FAILURE_NO_FILE]: s__('Pipelines|No CI file found in this repository, please add one.'),
+ [LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
+ [COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
+ },
+ methods: {
+ handleBlobContentError(error = {}) {
+ const { networkError } = error;
+
+ const { response } = networkError;
+ if (response?.status === 404) {
+ // 404 for missing CI file
+ this.reportFailure(LOAD_FAILURE_NO_FILE);
+ } else if (response?.status === 400) {
+ // 400 for a missing ref when no default branch is set
+ this.reportFailure(LOAD_FAILURE_NO_REF);
+ } else {
+ this.reportFailure(LOAD_FAILURE_UNKNOWN);
+ }
+ },
+ dismissFailure() {
+ this.showFailureAlert = false;
+ },
+ reportFailure(type, reasons = []) {
+ this.showFailureAlert = true;
+ this.failureType = type;
+ this.failureReasons = reasons;
+ },
+ redirectToNewMergeRequest(sourceBranch) {
+ const url = mergeUrlParams(
+ {
+ [MR_SOURCE_BRANCH]: sourceBranch,
+ [MR_TARGET_BRANCH]: this.defaultBranch,
+ },
+ this.newMergeRequestPath,
+ );
+ redirectTo(url);
+ },
+ async onCommitSubmit(event) {
+ this.isSaving = true;
+ const { message, branch, openMergeRequest } = event;
+
+ try {
+ const {
+ data: {
+ commitCreate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: commitCiFileMutation,
+ variables: {
+ projectPath: this.projectPath,
+ branch,
+ startBranch: this.defaultBranch,
+ message,
+ filePath: this.ciConfigPath,
+ content: this.contentModel,
+ lastCommitId: this.commitId,
+ },
+ });
+
+ if (errors?.length) {
+ this.reportFailure(COMMIT_FAILURE, errors);
+ return;
+ }
+
+ if (openMergeRequest) {
+ this.redirectToNewMergeRequest(branch);
+ } else {
+ // Refresh the page to ensure commit is updated
+ refreshCurrentPage();
+ }
+ } catch (error) {
+ this.reportFailure(COMMIT_FAILURE, [error?.message]);
+ } finally {
+ this.isSaving = false;
+ }
+ },
+ onCommitCancel() {
+ this.contentModel = this.content;
+ },
+ },
};
</script>
<template>
<div class="gl-mt-4">
- <gl-alert v-if="error" :dismissible="false" variant="danger">{{ errorMessage }}</gl-alert>
+ <gl-alert
+ v-if="showFailureAlert"
+ :variant="failure.variant"
+ :dismissible="true"
+ @dismiss="dismissFailure"
+ >
+ {{ failure.text }}
+ <ul v-if="failureReasons.length" class="gl-mb-0">
+ <li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
+ </ul>
+ </gl-alert>
<div class="gl-mt-4">
- <gl-loading-icon v-if="loading" size="lg" />
- <div v-else class="file-editor">
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
+ <div v-else class="file-editor gl-mb-3">
<gl-tabs>
<!-- editor should be mounted when its tab is visible, so the container has a size -->
<gl-tab :title="$options.i18n.tabEdit" :lazy="!editorIsReady">
<!-- editor should be mounted only once, when the tab is displayed -->
- <text-editor v-model="content" @editor-ready="editorIsReady = true" />
+ <text-editor v-model="contentModel" @editor-ready="editorIsReady = true" />
</gl-tab>
<gl-tab :title="$options.i18n.tabGraph">
@@ -103,6 +247,13 @@ export default {
</gl-tab>
</gl-tabs>
</div>
+ <commit-form
+ :default-branch="defaultBranch"
+ :default-message="defaultCommitMessage"
+ :is-saving="isSaving"
+ @cancel="onCommitCancel"
+ @submit="onCommitSubmit"
+ />
</div>
</div>
</template>
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 e52afe08336..1ea71610897 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_actions.vue
@@ -32,7 +32,7 @@ export default {
if (action.scheduled_at) {
const confirmationMessage = sprintf(
s__(
- "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes.",
+ 'DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes.',
),
{ jobName: action.name },
);
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 7afbb59cbd6..4b4fb6082c6 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,6 +1,13 @@
<script>
-import { mapGetters } from 'vuex';
-import { GlModalDirective, GlTooltipDirective, GlFriendlyWrap, GlIcon, GlButton } from '@gitlab/ui';
+import { mapState, mapGetters, mapActions } from 'vuex';
+import {
+ GlModalDirective,
+ GlTooltipDirective,
+ GlFriendlyWrap,
+ GlIcon,
+ GlButton,
+ GlPagination,
+} from '@gitlab/ui';
import { __ } from '~/locale';
import TestCaseDetails from './test_case_details.vue';
@@ -10,6 +17,7 @@ export default {
GlIcon,
GlFriendlyWrap,
GlButton,
+ GlPagination,
TestCaseDetails,
},
directives: {
@@ -24,11 +32,15 @@ export default {
},
},
computed: {
- ...mapGetters(['getSuiteTests']),
+ ...mapState(['pageInfo']),
+ ...mapGetters(['getSuiteTests', 'getSuiteTestCount']),
hasSuites() {
return this.getSuiteTests.length > 0;
},
},
+ methods: {
+ ...mapActions(['setPage']),
+ },
wrapSymbols: ['::', '#', '.', '_', '-', '/', '\\'],
};
</script>
@@ -129,6 +141,14 @@ export default {
</div>
</div>
</div>
+
+ <gl-pagination
+ v-model="pageInfo.page"
+ class="gl-display-flex gl-justify-content-center"
+ :per-page="pageInfo.perPage"
+ :total-items="getSuiteTestCount"
+ @input="setPage"
+ />
</div>
<div v-else>
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
index f10bbeec77c..3c664457756 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -47,6 +47,7 @@ export const fetchTestSuite = ({ state, commit, dispatch }, index) => {
});
};
+export const setPage = ({ commit }, page) => commit(types.SET_PAGE, page);
export const setSelectedSuiteIndex = ({ commit }, data) =>
commit(types.SET_SELECTED_SUITE_INDEX, data);
export const removeSelectedSuiteIndex = ({ commit }) =>
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
index c123014756d..56f769c00fa 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
@@ -14,5 +14,10 @@ export const getSelectedSuite = state =>
export const getSuiteTests = state => {
const { test_cases: testCases = [] } = getSelectedSuite(state);
- return testCases.map(addIconStatus);
+ const { page, perPage } = state.pageInfo;
+ const start = (page - 1) * perPage;
+
+ return testCases.map(addIconStatus).slice(start, start + perPage);
};
+
+export const getSuiteTestCount = state => getSelectedSuite(state)?.test_cases?.length || 0;
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 52345888cb0..803f6bf60b1 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
@@ -1,3 +1,4 @@
+export const SET_PAGE = 'SET_PAGE';
export const SET_SELECTED_SUITE_INDEX = 'SET_SELECTED_SUITE_INDEX';
export const SET_SUMMARY = 'SET_SUMMARY';
export const SET_SUITE = 'SET_SUITE';
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
index 3652a12a6ba..cf0bf8483dd 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
@@ -1,6 +1,14 @@
import * as types from './mutation_types';
export default {
+ [types.SET_PAGE](state, page) {
+ Object.assign(state, {
+ pageInfo: Object.assign(state.pageInfo, {
+ page,
+ }),
+ });
+ },
+
[types.SET_SUITE](state, { suite = {}, index = null }) {
state.testReports.test_suites[index] = { ...suite, hasFullSuite: true };
},
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js
index af79521d68a..7f5da549a9d 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/state.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js
@@ -4,4 +4,8 @@ export default ({ summaryEndpoint = '', suiteEndpoint = '' }) => ({
testReports: {},
selectedSuiteIndex: null,
isLoading: false,
+ pageInfo: {
+ page: 1,
+ perPage: 20,
+ },
});
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index db2b0856e1b..f7d823802b6 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -4,110 +4,116 @@ import $ from 'jquery';
import Api from './api';
import ProjectSelectComboButton from './project_select_combo_button';
import { s__ } from './locale';
+import { loadCSSFile } from './lib/utils/css_utils';
const projectSelect = () => {
- $('.ajax-project-select').each(function(i, select) {
- let placeholder;
- const simpleFilter = $(select).data('simpleFilter') || false;
- const isInstantiated = $(select).data('select2');
- this.groupId = $(select).data('groupId');
- this.userId = $(select).data('userId');
- this.includeGroups = $(select).data('includeGroups');
- this.allProjects = $(select).data('allProjects') || false;
- this.orderBy = $(select).data('orderBy') || 'id';
- this.withIssuesEnabled = $(select).data('withIssuesEnabled');
- this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
- this.withShared =
- $(select).data('withShared') === undefined ? true : $(select).data('withShared');
- this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
- this.allowClear = $(select).data('allowClear') || false;
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $('.ajax-project-select').each(function(i, select) {
+ let placeholder;
+ const simpleFilter = $(select).data('simpleFilter') || false;
+ const isInstantiated = $(select).data('select2');
+ this.groupId = $(select).data('groupId');
+ this.userId = $(select).data('userId');
+ this.includeGroups = $(select).data('includeGroups');
+ this.allProjects = $(select).data('allProjects') || false;
+ this.orderBy = $(select).data('orderBy') || 'id';
+ this.withIssuesEnabled = $(select).data('withIssuesEnabled');
+ this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
+ this.withShared =
+ $(select).data('withShared') === undefined ? true : $(select).data('withShared');
+ this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
+ this.allowClear = $(select).data('allowClear') || false;
- placeholder = s__('ProjectSelect|Search for project');
- if (this.includeGroups) {
- placeholder += s__('ProjectSelect| or group');
- }
-
- $(select).select2({
- placeholder,
- minimumInputLength: 0,
- query: query => {
- let projectsCallback;
- const finalCallback = function(projects) {
- const data = {
- results: projects,
- };
- return query.callback(data);
- };
+ placeholder = s__('ProjectSelect|Search for project');
if (this.includeGroups) {
- projectsCallback = function(projects) {
- const groupsCallback = function(groups) {
- const data = groups.concat(projects);
- return finalCallback(data);
- };
- return Api.groups(query.term, {}, groupsCallback);
- };
- } else {
- projectsCallback = finalCallback;
- }
- if (this.groupId) {
- return Api.groupProjects(
- this.groupId,
- query.term,
- {
- with_issues_enabled: this.withIssuesEnabled,
- with_merge_requests_enabled: this.withMergeRequestsEnabled,
- with_shared: this.withShared,
- include_subgroups: this.includeProjectsInSubgroups,
- order_by: 'similarity',
- },
- projectsCallback,
- );
- } else if (this.userId) {
- return Api.userProjects(
- this.userId,
- query.term,
- {
- with_issues_enabled: this.withIssuesEnabled,
- with_merge_requests_enabled: this.withMergeRequestsEnabled,
- with_shared: this.withShared,
- include_subgroups: this.includeProjectsInSubgroups,
- },
- projectsCallback,
- );
+ placeholder += s__('ProjectSelect| or group');
}
- return Api.projects(
- query.term,
- {
- order_by: this.orderBy,
- with_issues_enabled: this.withIssuesEnabled,
- with_merge_requests_enabled: this.withMergeRequestsEnabled,
- membership: !this.allProjects,
+
+ $(select).select2({
+ placeholder,
+ minimumInputLength: 0,
+ query: query => {
+ let projectsCallback;
+ const finalCallback = function(projects) {
+ const data = {
+ results: projects,
+ };
+ return query.callback(data);
+ };
+ if (this.includeGroups) {
+ projectsCallback = function(projects) {
+ const groupsCallback = function(groups) {
+ const data = groups.concat(projects);
+ return finalCallback(data);
+ };
+ return Api.groups(query.term, {}, groupsCallback);
+ };
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (this.groupId) {
+ return Api.groupProjects(
+ this.groupId,
+ query.term,
+ {
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ with_shared: this.withShared,
+ include_subgroups: this.includeProjectsInSubgroups,
+ order_by: 'similarity',
+ },
+ projectsCallback,
+ );
+ } else if (this.userId) {
+ return Api.userProjects(
+ this.userId,
+ query.term,
+ {
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ with_shared: this.withShared,
+ include_subgroups: this.includeProjectsInSubgroups,
+ },
+ projectsCallback,
+ );
+ }
+ return Api.projects(
+ query.term,
+ {
+ order_by: this.orderBy,
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ membership: !this.allProjects,
+ },
+ projectsCallback,
+ );
+ },
+ id(project) {
+ if (simpleFilter) return project.id;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
+ });
+ },
+ text(project) {
+ return project.name_with_namespace || project.name;
},
- projectsCallback,
- );
- },
- id(project) {
- if (simpleFilter) return project.id;
- return JSON.stringify({
- name: project.name,
- url: project.web_url,
- });
- },
- text(project) {
- return project.name_with_namespace || project.name;
- },
- initSelection(el, callback) {
- return Api.project(el.val()).then(({ data }) => callback(data));
- },
+ initSelection(el, callback) {
+ // eslint-disable-next-line promise/no-nesting
+ return Api.project(el.val()).then(({ data }) => callback(data));
+ },
- allowClear: this.allowClear,
+ allowClear: this.allowClear,
- dropdownCssClass: 'ajax-project-dropdown',
- });
- if (isInstantiated || simpleFilter) return select;
- return new ProjectSelectComboButton(select);
- });
+ dropdownCssClass: 'ajax-project-dropdown',
+ });
+ if (isInstantiated || simpleFilter) return select;
+ return new ProjectSelectComboButton(select);
+ });
+ })
+ .catch(() => {});
};
export default () => {
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
index d3b5f532dc1..865dd23bd80 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import AccessorUtilities from './lib/utils/accessor';
+import { loadCSSFile } from './lib/utils/css_utils';
export default class ProjectSelectComboButton {
constructor(select) {
@@ -46,9 +47,14 @@ export default class ProjectSelectComboButton {
openDropdown(event) {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- $(event.currentTarget)
- .siblings('.project-item-select')
- .select2('open');
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $(event.currentTarget)
+ .siblings('.project-item-select')
+ .select2('open');
+ })
+ .catch(() => {});
})
.catch(() => {});
}
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 f404e6030f4..2e16071e563 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
@@ -12,6 +12,7 @@ import ciCdProjectIllustration from '../illustrations/ci-cd-project.svg';
const BLANK_PANEL = 'blank_project';
const CI_CD_PANEL = 'cicd_for_external_repo';
+const LAST_ACTIVE_TAB_KEY = 'new_project_last_active_tab';
const PANELS = [
{
name: BLANK_PANEL,
@@ -105,7 +106,7 @@ export default {
this.handleLocationHashChange();
if (this.hasErrors) {
- this.activeTab = BLANK_PANEL;
+ this.activeTab = localStorage.getItem(LAST_ACTIVE_TAB_KEY) || BLANK_PANEL;
}
window.addEventListener('hashchange', () => {
@@ -127,6 +128,9 @@ export default {
handleLocationHashChange() {
this.activeTab = window.location.hash.substring(1) || null;
+ if (this.activeTab) {
+ localStorage.setItem(LAST_ACTIVE_TAB_KEY, this.activeTab);
+ }
},
},
diff --git a/app/assets/javascripts/registry/explorer/pages/index.vue b/app/assets/javascripts/registry/explorer/pages/index.vue
index 4ac0bca84c1..dca63e1a569 100644
--- a/app/assets/javascripts/registry/explorer/pages/index.vue
+++ b/app/assets/javascripts/registry/explorer/pages/index.vue
@@ -1,7 +1,3 @@
-<script>
-export default {};
-</script>
-
<template>
<div>
<router-view ref="router-view" />
diff --git a/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue b/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue
new file mode 100644
index 00000000000..d75fb31fd98
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormSelect,
+ },
+ props: {
+ formOptions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ name: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group :id="`${name}-form-group`" :label-for="name" :label="label">
+ <gl-form-select :id="name" :value="value" :disabled="disabled" @input="$emit('input', $event)">
+ <option
+ v-for="option in formOptions"
+ :key="option.key"
+ :value="option.key"
+ data-testid="option"
+ >
+ {{ option.label }}
+ </option>
+ </gl-form-select>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/registry/settings/components/expiration_run_text.vue b/app/assets/javascripts/registry/settings/components/expiration_run_text.vue
new file mode 100644
index 00000000000..186ad2f34b9
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_run_text.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { NEXT_CLEANUP_LABEL, NOT_SCHEDULED_POLICY_TEXT } from '~/registry/settings/constants';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: NOT_SCHEDULED_POLICY_TEXT,
+ },
+ },
+ i18n: {
+ NEXT_CLEANUP_LABEL,
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ id="expiration-policy-info-text-group"
+ :label="$options.i18n.NEXT_CLEANUP_LABEL"
+ label-for="expiration-policy-info-text"
+ >
+ <gl-form-input id="expiration-policy-info-text" class="gl-pl-0!" plaintext :value="value" />
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/registry/settings/components/expiration_textarea.vue b/app/assets/javascripts/registry/settings/components/expiration_textarea.vue
new file mode 100644
index 00000000000..1e1194ebb5c
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_textarea.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlFormGroup, GlFormTextarea, GlSprintf, GlLink } from '@gitlab/ui';
+import { NAME_REGEX_LENGTH, TEXT_AREA_INVALID_FEEDBACK } from '../constants';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormTextarea,
+ GlSprintf,
+ GlLink,
+ },
+ inject: ['tagsRegexHelpPagePath'],
+ props: {
+ error: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ name: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ placeholder: {
+ type: String,
+ required: true,
+ },
+ description: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ textAreaLengthErrorMessage() {
+ return this.isInputValid(this.value) ? '' : TEXT_AREA_INVALID_FEEDBACK;
+ },
+ textAreaValidation() {
+ const nameRegexErrors = this.error || this.textAreaLengthErrorMessage;
+ return {
+ state: nameRegexErrors === null ? null : !nameRegexErrors,
+ message: nameRegexErrors,
+ };
+ },
+ internalValue: {
+ get() {
+ return this.value;
+ },
+ set(value) {
+ this.$emit('input', value);
+ this.$emit('validation', this.isInputValid(value));
+ },
+ },
+ },
+ methods: {
+ isInputValid(value) {
+ return !value || value.length <= NAME_REGEX_LENGTH;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ :id="`${name}-form-group`"
+ :label-for="name"
+ :state="textAreaValidation.state"
+ :invalid-feedback="textAreaValidation.message"
+ >
+ <template #label>
+ <span data-testid="label">
+ <gl-sprintf :message="label">
+ <template #italic="{content}">
+ <i>{{ content }}</i>
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ <gl-form-textarea
+ :id="name"
+ v-model="internalValue"
+ :placeholder="placeholder"
+ :state="textAreaValidation.state"
+ :disabled="disabled"
+ trim
+ />
+ <template #description>
+ <span data-testid="description" class="gl-text-gray-400">
+ <gl-sprintf :message="description">
+ <template #link="{content}">
+ <gl-link :href="tagsRegexHelpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/registry/settings/components/expiration_toggle.vue b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue
new file mode 100644
index 00000000000..9dabe8ac51a
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlFormGroup, GlToggle, GlSprintf } from '@gitlab/ui';
+import { ENABLED_TEXT, DISABLED_TEXT, ENABLE_TOGGLE_DESCRIPTION } from '../constants';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlToggle,
+ GlSprintf,
+ },
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ value: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ i18n: {
+ ENABLE_TOGGLE_DESCRIPTION,
+ },
+ computed: {
+ enabled: {
+ get() {
+ return this.value;
+ },
+ set(value) {
+ this.$emit('input', value);
+ },
+ },
+ toggleStatusText() {
+ return this.enabled ? ENABLED_TEXT : DISABLED_TEXT;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group id="expiration-policy-toggle-group" label-for="expiration-policy-toggle">
+ <div class="gl-display-flex">
+ <gl-toggle id="expiration-policy-toggle" v-model="enabled" :disabled="disabled" />
+ <span class="gl-ml-5 gl-line-height-24" data-testid="description">
+ <gl-sprintf :message="$options.i18n.ENABLE_TOGGLE_DESCRIPTION">
+ <template #toggleStatus>
+ <strong>{{ toggleStatusText }}</strong>
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/registry/settings/constants.js b/app/assets/javascripts/registry/settings/constants.js
index e790658f491..bc3ec3104ad 100644
--- a/app/assets/javascripts/registry/settings/constants.js
+++ b/app/assets/javascripts/registry/settings/constants.js
@@ -1,6 +1,6 @@
import { s__, __ } from '~/locale';
-export const SET_CLEANUP_POLICY_BUTTON = s__('ContainerRegistry|Set cleanup policy');
+export const SET_CLEANUP_POLICY_BUTTON = __('Save');
export const CLEANUP_POLICY_CARD_HEADER = s__('ContainerRegistry|Tag expiration policy');
export const UNAVAILABLE_FEATURE_TITLE = s__(
`ContainerRegistry|Cleanup policy for tags is disabled`,
@@ -12,3 +12,46 @@ export const UNAVAILABLE_USER_FEATURE_TEXT = __(`Please contact your administrat
export const UNAVAILABLE_ADMIN_FEATURE_TEXT = s__(
`ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature.`,
);
+
+export const TEXT_AREA_INVALID_FEEDBACK = s__(
+ 'ContainerRegistry|The value of this input should be less than 256 characters',
+);
+
+export const KEEP_HEADER_TEXT = s__('ContainerRegistry|Keep these tags');
+export const KEEP_INFO_TEXT = s__(
+ 'ContainerRegistry|Tags that match these rules will always be %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag will always be kept.',
+);
+export const KEEP_N_LABEL = s__('ContainerRegistry|Keep the most recent:');
+export const NAME_REGEX_KEEP_LABEL = s__('ContainerRegistry|Keep tags matching:');
+export const NAME_REGEX_KEEP_PLACEHOLDER = 'production-v.*';
+export const NAME_REGEX_KEEP_DESCRIPTION = s__(
+ 'ContainerRegistry|Tags with names matching this regex pattern will be kept. %{linkStart}More information%{linkEnd}',
+);
+
+export const REMOVE_HEADER_TEXT = s__('ContainerRegistry|Remove these tags');
+export const REMOVE_INFO_TEXT = s__(
+ 'ContainerRegistry|Tags that match these rules will be %{strongStart}removed%{strongEnd}, unless kept by a rule above.',
+);
+export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Remove tags older than:');
+export const NAME_REGEX_LABEL = s__('ContainerRegistry|Remove tags matching:');
+export const NAME_REGEX_PLACEHOLDER = '.*';
+export const NAME_REGEX_DESCRIPTION = s__(
+ 'ContainerRegistry|Tags with names matching this regex pattern will be removed. %{linkStart}More information%{linkEnd}',
+);
+
+export const ENABLED_TEXT = __('Enabled');
+export const DISABLED_TEXT = __('Disabled');
+
+export const ENABLE_TOGGLE_DESCRIPTION = s__(
+ 'ContainerRegistry|%{toggleStatus} - Tags matching the rules defined below will be automatically scheduled for deletion.',
+);
+
+export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup every:');
+
+export const NEXT_CLEANUP_LABEL = s__('ContainerRegistry|Next cleanup scheduled to run on:');
+export const NOT_SCHEDULED_POLICY_TEXT = s__('ContainerRegistry|Not yet scheduled');
+export const EXPIRATION_POLICY_FOOTER_NOTE = s__(
+ 'ContainerRegistry|Note: Any policy update will result in a change to the scheduled run date and time',
+);
+
+export const NAME_REGEX_LENGTH = 255;
diff --git a/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql b/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql
index 224e0ed9472..1d6c89133af 100644
--- a/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql
+++ b/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql
@@ -5,4 +5,5 @@ fragment ContainerExpirationPolicyFields on ContainerExpirationPolicy {
nameRegex
nameRegexKeep
olderThan
+ nextRunAt
}
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
index f7b1c5abd3a..6a4584b1b28 100644
--- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -13,7 +13,13 @@ export default () => {
if (!el) {
return null;
}
- const { projectPath, isAdmin, adminSettingsPath, enableHistoricEntries } = el.dataset;
+ const {
+ isAdmin,
+ enableHistoricEntries,
+ projectPath,
+ adminSettingsPath,
+ tagsRegexHelpPagePath,
+ } = el.dataset;
return new Vue({
el,
apolloProvider,
@@ -21,10 +27,11 @@ export default () => {
RegistrySettingsApp,
},
provide: {
- projectPath,
isAdmin: parseBoolean(isAdmin),
- adminSettingsPath,
enableHistoricEntries: parseBoolean(enableHistoricEntries),
+ projectPath,
+ adminSettingsPath,
+ tagsRegexHelpPagePath,
},
render(createElement) {
return createElement('registry-settings-app', {});
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index f245e2bfd2f..0e9975ea81f 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -3,7 +3,7 @@ import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
import IssuesList from './issues_list.vue';
-import { status } from '../constants';
+import { status, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '../constants';
export default {
name: 'ReportSection',
@@ -152,12 +152,12 @@ export default {
},
slotName() {
if (this.isSuccess) {
- return 'success';
+ return SLOT_SUCCESS;
} else if (this.isLoading) {
- return 'loading';
+ return SLOT_LOADING;
}
- return 'error';
+ return SLOT_ERROR;
},
},
methods: {
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js
index b3905cbfcfb..9250bfd7678 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/reports/constants.js
@@ -18,10 +18,18 @@ export const ICON_SUCCESS = 'success';
export const ICON_NOTFOUND = 'notfound';
export const status = {
- LOADING: 'LOADING',
- ERROR: 'ERROR',
- SUCCESS: 'SUCCESS',
+ LOADING,
+ ERROR,
+ SUCCESS,
};
export const ACCESSIBILITY_ISSUE_ERROR = 'error';
export const ACCESSIBILITY_ISSUE_WARNING = 'warning';
+
+/**
+ * Slot names for the ReportSection component, corresponding to the success,
+ * loading and error statuses.
+ */
+export const SLOT_SUCCESS = 'success';
+export const SLOT_LOADING = 'loading';
+export const SLOT_ERROR = 'error';
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
index cf6a0a4a151..3c1b3afe889 100644
--- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -1,9 +1,11 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { n__ } from '~/locale';
import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
export default {
components: {
+ GlButton,
UncollapsedAssigneeList,
},
inject: ['rootPath'],
@@ -27,9 +29,15 @@ export default {
<template>
<div class="gl-display-flex gl-flex-direction-column">
<div v-if="emptyUsers" data-testid="none">
- <span>
- {{ __('None') }}
- </span>
+ <span> {{ __('None') }} -</span>
+ <gl-button
+ data-testid="assign-yourself"
+ category="tertiary"
+ variant="link"
+ @click="$emit('assign-self')"
+ >
+ <span class="gl-text-gray-400">{{ __('assign yourself') }}</span>
+ </gl-button>
</div>
<uncollapsed-assignee-list v-else :users="users" :root-path="rootPath" />
</div>
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index 51719df313f..1e3e870ec83 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -1,19 +1,18 @@
<script>
-import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
const MARK_TEXT = __('Mark as done');
const TODO_TEXT = __('Add a To-Do');
export default {
- directives: {
- tooltip,
- },
components: {
GlIcon,
GlLoadingIcon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
issuableId: {
type: Number,
@@ -71,16 +70,13 @@ export default {
<template>
<button
- v-tooltip
+ v-gl-tooltip.left.viewport
:class="buttonClasses"
:title="buttonTooltip"
:aria-label="buttonLabel"
:data-issuable-id="issuableId"
:data-issuable-type="issuableType"
type="button"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
@click="handleButtonClick"
>
<gl-icon
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index dccd6807f13..19846026726 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -15,6 +15,7 @@ import { parseBoolean, spriteIcon } from '../lib/utils/common_utils';
import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { fixTitle, dispose } from '~/tooltips';
+import { loadCSSFile } from '../lib/utils/css_utils';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
@@ -592,92 +593,97 @@ function UsersSelect(currentUser, els, options = {}) {
if ($('.ajax-users-select').length) {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
- $('.ajax-users-select').each((i, select) => {
- const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
- options.skipLdap = $(select).hasClass('skip_ldap');
- const showNullUser = $(select).data('nullUser');
- const showAnyUser = $(select).data('anyUser');
- const showEmailUser = $(select).data('emailUser');
- const firstUser = $(select).data('firstUser');
- return $(select).select2({
- placeholder: __('Search for a user'),
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query(query) {
- return userSelect.users(query.term, options, users => {
- let name;
- const data = {
- results: users,
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- const ref = data.results;
-
- for (let index = 0, len = ref.length; index < len; index += 1) {
- const obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $('.ajax-users-select').each((i, select) => {
+ const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ const showNullUser = $(select).data('nullUser');
+ const showAnyUser = $(select).data('anyUser');
+ const showEmailUser = $(select).data('emailUser');
+ const firstUser = $(select).data('firstUser');
+ return $(select).select2({
+ placeholder: __('Search for a user'),
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query(query) {
+ return userSelect.users(query.term, options, users => {
+ let name;
+ const data = {
+ results: users,
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ // Move current user to the front of the list
+ const ref = data.results;
+
+ for (let index = 0, len = ref.length; index < len; index += 1) {
+ const obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
+ }
+ }
+ if (showNullUser) {
+ const nullUser = {
+ name: s__('UsersSelect|Unassigned'),
+ id: 0,
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = s__('UsersSelect|Any User');
+ }
+ const anyUser = {
+ name,
+ id: null,
+ };
+ data.results.unshift(anyUser);
}
}
- }
- if (showNullUser) {
- const nullUser = {
- name: s__('UsersSelect|Unassigned'),
- id: 0,
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = s__('UsersSelect|Any User');
+ if (
+ showEmailUser &&
+ data.results.length === 0 &&
+ query.term.match(/^[^@]+@[^@]+$/)
+ ) {
+ const trimmed = query.term.trim();
+ const emailUser = {
+ name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
+ username: trimmed,
+ id: trimmed,
+ invite: true,
+ };
+ data.results.unshift(emailUser);
}
- const anyUser = {
- name,
- id: null,
- };
- data.results.unshift(anyUser);
- }
- }
- if (
- showEmailUser &&
- data.results.length === 0 &&
- query.term.match(/^[^@]+@[^@]+$/)
- ) {
- const trimmed = query.term.trim();
- const emailUser = {
- name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
- username: trimmed,
- id: trimmed,
- invite: true,
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
+ return query.callback(data);
+ });
+ },
+ initSelection() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.initSelection.apply(userSelect, args);
+ },
+ formatResult() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.formatResult.apply(userSelect, args);
+ },
+ formatSelection() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.formatSelection.apply(userSelect, args);
+ },
+ dropdownCssClass: 'ajax-users-dropdown',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
});
- },
- initSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.initSelection.apply(userSelect, args);
- },
- formatResult() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatResult.apply(userSelect, args);
- },
- formatSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatSelection.apply(userSelect, args);
- },
- dropdownCssClass: 'ajax-users-dropdown',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
- });
+ });
+ })
+ .catch(() => {});
})
.catch(() => {});
}
@@ -790,7 +796,7 @@ UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
const mergeIcon =
issuableType === 'merge_request' && !user.can_merge
- ? '<i class="fa fa-exclamation-triangle merge-icon"></i>'
+ ? `${spriteIcon('warning-solid', 's12 merge-icon')}`
: '';
return `<span class="position-relative mr-2">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
index 66de4f8b682..29d067a46a6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/constants.js
@@ -6,6 +6,7 @@ export const RUNNING = 'running';
export const SUCCESS = 'success';
export const FAILED = 'failed';
export const CANCELED = 'canceled';
+export const SKIPPED = 'skipped';
// ACTION STATUSES
export const STOPPING = 'stopping';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
index 2f922b990d9..390469dec24 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
@@ -4,7 +4,15 @@ import { __ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import MemoryUsage from './memory_usage.vue';
-import { MANUAL_DEPLOY, WILL_DEPLOY, RUNNING, SUCCESS, FAILED, CANCELED } from './constants';
+import {
+ MANUAL_DEPLOY,
+ WILL_DEPLOY,
+ RUNNING,
+ SUCCESS,
+ FAILED,
+ CANCELED,
+ SKIPPED,
+} from './constants';
export default {
name: 'DeploymentInfo',
@@ -38,6 +46,7 @@ export default {
[SUCCESS]: __('Deployed to'),
[FAILED]: __('Failed to deploy to'),
[CANCELED]: __('Canceled deployment to'),
+ [SKIPPED]: __('Skipped deployment to'),
},
computed: {
deployTimeago() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index d5fdbe726e9..6628ab7be83 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -7,12 +7,14 @@ import {
GlDropdownSectionHeader,
GlDropdownItem,
GlTooltipDirective,
+ GlModalDirective,
} from '@gitlab/ui';
import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import MrWidgetIcon from './mr_widget_icon.vue';
+import MrWidgetHowToMergeModal from './mr_widget_how_to_merge_modal.vue';
export default {
name: 'MRWidgetHeader',
@@ -20,6 +22,7 @@ export default {
clipboardButton,
TooltipOnTruncate,
MrWidgetIcon,
+ MrWidgetHowToMergeModal,
GlButton,
GlDropdown,
GlDropdownSectionHeader,
@@ -27,6 +30,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ GlModalDirective,
},
props: {
mr: {
@@ -82,6 +86,9 @@ export default {
)
: '';
},
+ isFork() {
+ return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
+ },
},
};
</script>
@@ -140,13 +147,22 @@ export default {
</gl-button>
</span>
<gl-button
+ v-gl-modal-directive="'modal-merge-info'"
:disabled="mr.sourceBranchRemoved"
- data-target="#modal_merge_info"
- data-toggle="modal"
class="js-check-out-branch gl-mr-3"
>
{{ s__('mrWidget|Check out branch') }}
</gl-button>
+ <mr-widget-how-to-merge-modal
+ :is-fork="isFork"
+ :can-merge="mr.canMerge"
+ :source-branch="mr.sourceBranch"
+ :source-project="mr.sourceProject"
+ :source-project-path="mr.sourceProjectFullPath"
+ :target-branch="mr.targetBranch"
+ :source-project-default-url="mr.sourceProjectDefaultUrl"
+ :reviewing-docs-path="mr.reviewingDocsPath"
+ />
</template>
<gl-dropdown
v-gl-tooltip
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
new file mode 100644
index 00000000000..957356eab27
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
@@ -0,0 +1,166 @@
+<script>
+/* eslint-disable @gitlab/require-i18n-strings */
+import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { __ } from '~/locale';
+
+export default {
+ i18n: {
+ steps: {
+ step1: {
+ label: __('Step 1.'),
+ help: __('Fetch and check out the branch for this merge request'),
+ },
+ step2: {
+ label: __('Step 2.'),
+ help: __('Review the changes locally'),
+ },
+ step3: {
+ label: __('Step 3.'),
+ help: __('Merge the branch and fix any conflicts that come up'),
+ },
+ step4: {
+ label: __('Step 4.'),
+ help: __('Push the result of the merge to GitLab'),
+ },
+ },
+ copyCommands: __('Copy commands'),
+ tip: __(
+ '%{strongStart}Tip:%{strongEnd} You can also checkout merge requests locally by %{linkStart}following these guidelines%{linkEnd}',
+ ),
+ title: __('Check out, review, and merge locally'),
+ },
+ components: {
+ GlModal,
+ ClipboardButton,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ canMerge: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isFork: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ sourceBranch: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ sourceProjectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ targetBranch: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ sourceProjectDefaultUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ reviewingDocsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ mergeInfo1() {
+ return this.isFork
+ ? `git fetch "${this.sourceProjectDefaultUrl}" ${this.sourceBranch}\ngit checkout -b "${this.sourceProjectPath}-${this.sourceBranch}" FETCH_HEAD`
+ : `git fetch origin\ngit checkout -b "${this.sourceBranch}" "origin/${this.sourceBranch}"`;
+ },
+ mergeInfo2() {
+ return this.isFork
+ ? `git fetch origin\ngit checkout "${this.targetBranch}"\ngit merge --no-ff "${this.sourceProjectPath}-${this.sourceBranch}"`
+ : `git fetch origin\ngit checkout "${this.targetBranch}"\ngit merge --no-ff " ${this.sourceBranch}"`;
+ },
+ mergeInfo3() {
+ return this.canMerge
+ ? `git push origin "${this.targetBranch}"`
+ : __('Note that pushing to GitLab requires write access to this repository.');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal modal-id="modal-merge-info" :title="$options.i18n.title" no-fade hide-footer>
+ <p>
+ <strong>
+ {{ $options.i18n.steps.step1.label }}
+ </strong>
+ {{ $options.i18n.steps.step1.help }}
+ </p>
+ <div class="gl-display-flex">
+ <pre class="gl-overflow-scroll gl-w-full" data-testid="how-to-merge-instructions">{{
+ mergeInfo1
+ }}</pre>
+ <clipboard-button
+ :text="mergeInfo1"
+ :title="$options.i18n.copyCommands"
+ class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0"
+ />
+ </div>
+
+ <p>
+ <strong>
+ {{ $options.i18n.steps.step2.label }}
+ </strong>
+ {{ $options.i18n.steps.step2.help }}
+ </p>
+ <p>
+ <strong>
+ {{ $options.i18n.steps.step3.label }}
+ </strong>
+ {{ $options.i18n.steps.step3.help }}
+ </p>
+ <div class="gl-display-flex">
+ <pre class="gl-overflow-scroll gl-w-full" data-testid="how-to-merge-instructions">{{
+ mergeInfo2
+ }}</pre>
+ <clipboard-button
+ :text="mergeInfo2"
+ :title="$options.i18n.copyCommands"
+ class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0"
+ />
+ </div>
+ <p>
+ <strong>
+ {{ $options.i18n.steps.step4.label }}
+ </strong>
+ {{ $options.i18n.steps.step4.help }}
+ </p>
+ <div class="gl-display-flex">
+ <pre class="gl-overflow-scroll gl-w-full" data-testid="how-to-merge-instructions">{{
+ mergeInfo3
+ }}</pre>
+ <clipboard-button
+ :text="mergeInfo3"
+ :title="$options.i18n.copyCommands"
+ class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0"
+ />
+ </div>
+ <p v-if="reviewingDocsPath">
+ <gl-sprintf :message="$options.i18n.tip">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #link="{ content }">
+ <gl-link class="gl-display-inline-block" :href="reviewingDocsPath" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 55efd7e7d3b..dffe3cab904 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -1,7 +1,6 @@
<script>
import { isNumber } from 'lodash';
import ArtifactsApp from './artifacts_list_app.vue';
-import Deployment from './deployment/deployment.vue';
import MrWidgetContainer from './mr_widget_container.vue';
import MrWidgetPipeline from './mr_widget_pipeline.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -18,7 +17,7 @@ export default {
name: 'MrWidgetPipelineContainer',
components: {
ArtifactsApp,
- Deployment,
+ Deployment: () => import('./deployment/deployment.vue'),
MrWidgetContainer,
MrWidgetPipeline,
MergeTrainPositionIndicator: () =>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 82566682bca..bc23ca6b1fc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -1,10 +1,11 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
export default {
components: {
ciIcon,
+ GlButton,
GlLoadingIcon,
},
props: {
@@ -32,21 +33,23 @@ export default {
};
</script>
<template>
- <div class="d-flex align-self-start">
+ <div class="gl-display-flex gl-align-self-start">
<div class="square s24 h-auto d-flex-center gl-mr-3">
- <div v-if="isLoading" class="mr-widget-icon d-inline-flex">
- <gl-loading-icon size="md" class="mr-loading-icon d-inline-flex" />
+ <div v-if="isLoading" class="mr-widget-icon gl-display-inline-flex">
+ <gl-loading-icon size="md" class="mr-loading-icon gl-display-inline-flex" />
</div>
<ci-icon v-else :status="statusObj" :size="24" />
</div>
- <button
+ <gl-button
v-if="showDisabledButton"
type="button"
- class="js-disabled-merge-button btn btn-success btn-sm"
- disabled="true"
+ category="primary"
+ variant="success"
+ class="js-disabled-merge-button"
+ :disabled="true"
>
{{ s__('mrWidget|Merge') }}
- </button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index d421b744fa1..2df03fbc679 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
import { escape } from 'lodash';
+import { GlButton, GlModalDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
import StatusIcon from '../mr_widget_status_icon.vue';
@@ -9,6 +10,10 @@ export default {
name: 'MRWidgetConflicts',
components: {
StatusIcon,
+ GlButton,
+ },
+ directives: {
+ GlModalDirective,
},
props: {
/* TODO: This is providing all store and service down when it
@@ -89,22 +94,21 @@ To merge this request, first rebase locally.`)
</span>
</span>
<span v-if="showResolveButton" ref="popover">
- <a
+ <gl-button
:href="mr.conflictResolutionPath"
:disabled="mr.sourceBranchProtected"
- class="js-resolve-conflicts-button btn btn-default btn-sm"
+ class="js-resolve-conflicts-button"
>
{{ s__('mrWidget|Resolve conflicts') }}
- </a>
+ </gl-button>
</span>
- <button
+ <gl-button
v-if="mr.canMerge"
- class="js-merge-locally-button btn btn-default btn-sm"
- data-toggle="modal"
- data-target="#modal_merge_info"
+ v-gl-modal-directive="'modal-merge-info'"
+ class="js-merge-locally-button"
>
{{ s__('mrWidget|Merge locally') }}
- </button>
+ </gl-button>
</template>
</div>
</div>
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 ff0d065c71d..1c9909e7178 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
@@ -1,10 +1,11 @@
<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
import { SQUASH_BEFORE_MERGE } from '../../i18n';
export default {
components: {
GlIcon,
+ GlFormCheckbox,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -32,32 +33,23 @@ export default {
tooltipTitle() {
return this.isDisabled ? this.$options.i18n.tooltipTitle : null;
},
- tooltipFocusable() {
- return this.isDisabled ? '0' : null;
- },
},
};
</script>
<template>
- <div class="inline">
- <label
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-form-checkbox
v-gl-tooltip
- :class="{ 'gl-text-gray-400': isDisabled }"
- :tabindex="tooltipFocusable"
- data-testid="squashLabel"
+ :checked="value"
+ :disabled="isDisabled"
+ name="squash"
+ class="qa-squash-checkbox js-squash-checkbox gl-mb-0 gl-mr-2"
:title="tooltipTitle"
+ @change="checked => $emit('input', checked)"
>
- <input
- :checked="value"
- :disabled="isDisabled"
- type="checkbox"
- name="squash"
- class="qa-squash-checkbox js-squash-checkbox"
- @change="$emit('input', $event.target.checked)"
- />
{{ $options.i18n.checkboxLabel }}
- </label>
+ </gl-form-checkbox>
<a
v-if="helpPath"
v-gl-tooltip
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 190d790f584..433dcf2e219 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
@@ -16,7 +16,6 @@ import WidgetHeader from './components/mr_widget_header.vue';
import WidgetSuggestPipeline from './components/mr_widget_suggest_pipeline.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
-import Deployment from './components/deployment/deployment.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
import MergedState from './components/states/mr_widget_merged.vue';
@@ -63,7 +62,6 @@ export default {
'mr-widget-suggest-pipeline': WidgetSuggestPipeline,
'mr-widget-merge-help': WidgetMergeHelp,
MrWidgetPipelineContainer,
- Deployment,
'mr-widget-related-links': WidgetRelatedLinks,
MrWidgetAlertMessage,
'mr-widget-merged': MergedState,
@@ -155,10 +153,7 @@ export default {
},
shouldSuggestPipelines() {
return (
- gon.features?.suggestPipeline &&
- !this.mr.hasCI &&
- this.mr.mergeRequestAddCiConfigPath &&
- !this.mr.isDismissedSuggestPipeline
+ !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath && !this.mr.isDismissedSuggestPipeline
);
},
shouldRenderCodeQuality() {
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 8b235b20ad4..f50b6caf0f5 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
@@ -220,6 +220,7 @@ export default class MergeRequestStore {
this.sourceProjectFullPath = data.source_project_full_path;
this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path;
this.conflictsDocsPath = data.conflicts_docs_path;
+ this.reviewingDocsPath = data.reviewing_and_managing_merge_requests_docs_path;
this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
this.securityApprovalsHelpPagePath = data.security_approvals_help_page_path;
this.eligibleApproversDocsPath = data.eligible_approvers_docs_path;
@@ -229,6 +230,7 @@ export default class MergeRequestStore {
this.pipelinesEmptySvgPath = data.pipelines_empty_svg_path;
this.humanAccess = data.human_access;
this.newPipelinePath = data.new_project_pipeline_path;
+ this.sourceProjectDefaultUrl = data.source_project_default_url;
this.userCalloutsPath = data.user_callouts_path;
this.suggestPipelineFeatureId = data.suggest_pipeline_feature_id;
this.isDismissedSuggestPipeline = data.is_dismissed_suggest_pipeline;
@@ -240,6 +242,10 @@ export default class MergeRequestStore {
this.baseBlobPath = blobPath.base_path || '';
this.codequalityHelpPath = data.codequality_help_path;
this.codeclimate = data.codeclimate;
+
+ // Security reports
+ this.sastComparisonPath = data.sast_comparison_path;
+ this.secretScanningComparisonPath = data.secret_scanning_comparison_path;
}
get isNothingToMergeState() {
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index 7a687ea4ad0..6d5912df96b 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
import { groupBy } from 'lodash';
-import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { glEmojiTag } from '../../emoji';
import { __, sprintf } from '~/locale';
@@ -10,8 +10,8 @@ const NO_USER_ID = -1;
export default {
components: {
+ GlButton,
GlIcon,
- GlLoadingIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -64,7 +64,7 @@ export default {
methods: {
getAwardClassBindings(awardList) {
return {
- active: this.hasReactionByCurrentUser(awardList),
+ selected: this.hasReactionByCurrentUser(awardList),
disabled: this.currentUserId === NO_USER_ID,
};
},
@@ -150,40 +150,39 @@ export default {
<template>
<div class="awards js-awards-block">
- <button
+ <gl-button
v-for="awardList in groupedAwards"
:key="awardList.name"
v-gl-tooltip.viewport
+ class="gl-mr-3"
:class="awardList.classes"
:title="awardList.title"
data-testid="award-button"
- class="btn award-control"
- type="button"
@click="handleAward(awardList.name)"
>
- <span data-testid="award-html" v-html="awardList.html"></span>
- <span class="award-control-text js-counter">{{ awardList.list.length }}</span>
- </button>
+ <template #emoji>
+ <span class="award-emoji-block" data-testid="award-html" v-html="awardList.html"></span>
+ </template>
+ <span class="js-counter">{{ awardList.list.length }}</span>
+ </gl-button>
<div v-if="canAwardEmoji" class="award-menu-holder">
- <button
+ <gl-button
v-gl-tooltip.viewport
:class="addButtonClass"
- class="award-control btn js-add-award"
+ class="add-reaction-button js-add-award"
title="Add reaction"
:aria-label="__('Add reaction')"
- type="button"
>
- <span class="award-control-icon award-control-icon-neutral">
+ <span class="reaction-control-icon reaction-control-icon-neutral">
<gl-icon aria-hidden="true" name="slight-smile" />
</span>
- <span class="award-control-icon award-control-icon-positive">
+ <span class="reaction-control-icon reaction-control-icon-positive">
<gl-icon aria-hidden="true" name="smiley" />
</span>
- <span class="award-control-icon award-control-icon-super-positive">
- <gl-icon aria-hidden="true" name="smiley" />
+ <span class="reaction-control-icon reaction-control-icon-super-positive">
+ <gl-icon aria-hidden="true" name="smile" />
</span>
- <gl-loading-icon size="md" color="dark" class="award-control-icon-loading" />
- </button>
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
index d4c1808eec2..106dd7a3b97 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
@@ -1,3 +1 @@
export const HIGHLIGHT_CLASS_NAME = 'hll';
-
-export default {};
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 328c7e3fd32..55526dcc381 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -77,7 +77,7 @@ export default {
</script>
<template>
- <div>
+ <div data-testid="image-viewer">
<div :class="innerCssClasses" class="position-relative">
<img ref="contentImg" :src="path" @load="onImgLoad" /> <slot name="image-overlay"></slot>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
index 40708453d79..aaadc9766db 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
@@ -89,5 +89,3 @@ export const inputStringToIsoDate = (value, utc = false) => {
*/
export const isoDateToInputString = (date, utc = false) =>
dateformat(date, dateFormats.inputFormat, utc);
-
-export default {};
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index b4115b0c6a4..4d07d9fcfdd 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -143,6 +143,7 @@ export default {
:style="levelIndentation"
class="file-row-name"
data-qa-selector="file_name_content"
+ data-testid="file-row-name-container"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
>
<file-icon
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue
new file mode 100644
index 00000000000..c45666e69eb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue
@@ -0,0 +1,90 @@
+<script>
+import Tribute from 'tributejs';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils';
+
+export default {
+ errorMessage: __(
+ 'An error occurred while getting autocomplete data. Please refresh the page and try again.',
+ ),
+ props: {
+ autocompleteTypes: {
+ type: Array,
+ required: false,
+ default: () => Object.values(GfmAutocompleteType),
+ },
+ dataSources: {
+ type: Object,
+ required: false,
+ default: () => gl.GfmAutoComplete?.dataSources || {},
+ },
+ },
+ computed: {
+ config() {
+ return this.autocompleteTypes.map(type => ({
+ ...tributeConfig[type].config,
+ values: this.getValues(type),
+ }));
+ },
+ },
+ mounted() {
+ this.cache = {};
+ this.tribute = new Tribute({ collection: this.config });
+
+ const input = this.$slots.default?.[0]?.elm;
+ this.tribute.attach(input);
+ },
+ beforeDestroy() {
+ const input = this.$slots.default?.[0]?.elm;
+ this.tribute.detach(input);
+ },
+ methods: {
+ cacheAssignees() {
+ const isAssigneesLengthSame =
+ this.assignees?.length === SidebarMediator.singleton?.store?.assignees?.length;
+
+ if (!this.assignees || !isAssigneesLengthSame) {
+ this.assignees =
+ SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
+ }
+ },
+ filterValues(type) {
+ // The assignees AJAX response can come after the user first invokes autocomplete
+ // so we need to check more than once if we need to update the assignee cache
+ this.cacheAssignees();
+
+ return tributeConfig[type].filterValues
+ ? tributeConfig[type].filterValues({
+ assignees: this.assignees,
+ collection: this.cache[type],
+ fullText: this.$slots.default?.[0]?.elm?.value,
+ selectionStart: this.$slots.default?.[0]?.elm?.selectionStart,
+ })
+ : this.cache[type];
+ },
+ getValues(type) {
+ return (inputText, processValues) => {
+ if (this.cache[type]) {
+ processValues(this.filterValues(type));
+ } else if (this.dataSources[type]) {
+ axios
+ .get(this.dataSources[type])
+ .then(response => {
+ this.cache[type] = response.data;
+ processValues(this.filterValues(type));
+ })
+ .catch(() => createFlash({ message: this.$options.errorMessage }));
+ } else {
+ processValues([]);
+ }
+ };
+ },
+ },
+ render(createElement) {
+ return createElement('div', this.$slots.default);
+ },
+};
+</script>
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
new file mode 100644
index 00000000000..b2e995d0f17
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
@@ -0,0 +1,142 @@
+import { escape, last } from 'lodash';
+import { spriteIcon } from '~/lib/utils/common_utils';
+
+const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings
+
+const nonWordOrInteger = /\W|^\d+$/;
+
+export const GfmAutocompleteType = {
+ Issues: 'issues',
+ Labels: 'labels',
+ Members: 'members',
+ MergeRequests: 'mergeRequests',
+ Milestones: 'milestones',
+ Snippets: 'snippets',
+};
+
+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);
+}
+
+export const tributeConfig = {
+ [GfmAutocompleteType.Issues]: {
+ config: {
+ trigger: '#',
+ lookup: value => value.iid + value.title,
+ menuItemTemplate: ({ original }) =>
+ `<small>${original.reference || original.iid}</small> ${escape(original.title)}`,
+ selectTemplate: ({ original }) => original.reference || `#${original.iid}`,
+ },
+ },
+
+ [GfmAutocompleteType.Labels]: {
+ config: {
+ trigger: '~',
+ lookup: 'title',
+ menuItemTemplate: ({ original }) => `
+ <span class="dropdown-label-box" style="background: ${escape(original.color)};"></span>
+ ${escape(original.title)}`,
+ selectTemplate: ({ original }) =>
+ nonWordOrInteger.test(original.title)
+ ? `~"${escape(original.title)}"`
+ : `~${escape(original.title)}`,
+ },
+ filterValues({ collection, fullText, selectionStart }) {
+ if (doesCurrentLineStartWith('/label', fullText, selectionStart)) {
+ return collection.filter(label => !label.set);
+ }
+
+ if (doesCurrentLineStartWith('/unlabel', fullText, selectionStart)) {
+ return collection.filter(label => label.set);
+ }
+
+ return collection;
+ },
+ },
+
+ [GfmAutocompleteType.Members]: {
+ config: {
+ trigger: '@',
+ fillAttr: 'username',
+ lookup: value =>
+ value.type === groupType ? last(value.name.split(' / ')) : value.name + value.username,
+ menuItemTemplate: ({ original }) => {
+ const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
+ const noAvatarClasses = `${commonClasses} gl-rounded-small
+ gl-display-flex gl-align-items-center gl-justify-content-center`;
+
+ const avatar = original.avatar_url
+ ? `<img class="${commonClasses} gl-avatar-circle" src="${original.avatar_url}" alt="" />`
+ : `<div class="${noAvatarClasses}" aria-hidden="true">
+ ${original.username.charAt(0).toUpperCase()}</div>`;
+
+ let displayName = original.name;
+ let parentGroupOrUsername = `@${original.username}`;
+
+ if (original.type === groupType) {
+ const splitName = original.name.split(' / ');
+ displayName = splitName.pop();
+ parentGroupOrUsername = splitName.pop();
+ }
+
+ const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
+
+ const disabledMentionsIcon = original.mentionsDisabled
+ ? spriteIcon('notifications-off', 's16 gl-ml-3')
+ : '';
+
+ return `
+ <div class="gl-display-flex gl-align-items-center">
+ ${avatar}
+ <div class="gl-font-sm gl-line-height-normal gl-ml-3">
+ <div>${escape(displayName)}${count}</div>
+ <div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
+ </div>
+ ${disabledMentionsIcon}
+ </div>
+ `;
+ },
+ },
+ filterValues({ assignees, collection, fullText, selectionStart }) {
+ if (doesCurrentLineStartWith('/assign', fullText, selectionStart)) {
+ return collection.filter(member => !assignees.includes(member.username));
+ }
+
+ if (doesCurrentLineStartWith('/unassign', fullText, selectionStart)) {
+ return collection.filter(member => assignees.includes(member.username));
+ }
+
+ return collection;
+ },
+ },
+
+ [GfmAutocompleteType.MergeRequests]: {
+ config: {
+ trigger: '!',
+ lookup: value => value.iid + value.title,
+ menuItemTemplate: ({ original }) =>
+ `<small>${original.reference || original.iid}</small> ${escape(original.title)}`,
+ selectTemplate: ({ original }) => original.reference || `!${original.iid}`,
+ },
+ },
+
+ [GfmAutocompleteType.Milestones]: {
+ config: {
+ trigger: '%',
+ lookup: 'title',
+ menuItemTemplate: ({ original }) => escape(original.title),
+ selectTemplate: ({ original }) => `%"${escape(original.title)}"`,
+ },
+ },
+
+ [GfmAutocompleteType.Snippets]: {
+ config: {
+ trigger: '$',
+ fillAttr: 'id',
+ lookup: value => value.id + value.title,
+ menuItemTemplate: ({ original }) => `<small>${original.id}</small> ${escape(original.title)}`,
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
deleted file mode 100644
index dde7e3ebe13..00000000000
--- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue
+++ /dev/null
@@ -1,238 +0,0 @@
-<script>
-import { escape, last } from 'lodash';
-import Tribute from 'tributejs';
-import axios from '~/lib/utils/axios_utils';
-import { spriteIcon } from '~/lib/utils/common_utils';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-
-const AutoComplete = {
- Issues: 'issues',
- Labels: 'labels',
- Members: 'members',
- MergeRequests: 'mergeRequests',
- Milestones: 'milestones',
- Snippets: 'snippets',
-};
-
-const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings
-
-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 commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
- const noAvatarClasses = `${commonClasses} gl-rounded-small
- gl-display-flex gl-align-items-center gl-justify-content-center`;
-
- const avatar = original.avatar_url
- ? `<img class="${commonClasses} gl-avatar-circle" src="${original.avatar_url}" alt="" />`
- : `<div class="${noAvatarClasses}" aria-hidden="true">
- ${original.username.charAt(0).toUpperCase()}</div>`;
-
- let displayName = original.name;
- let parentGroupOrUsername = `@${original.username}`;
-
- if (original.type === groupType) {
- const splitName = original.name.split(' / ');
- displayName = splitName.pop();
- parentGroupOrUsername = splitName.pop();
- }
-
- const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
-
- const disabledMentionsIcon = original.mentionsDisabled
- ? spriteIcon('notifications-off', 's16 gl-ml-3')
- : '';
-
- return `
- <div class="gl-display-flex gl-align-items-center">
- ${avatar}
- <div class="gl-font-sm gl-line-height-normal gl-ml-3">
- <div>${escape(displayName)}${count}</div>
- <div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
- </div>
- ${disabledMentionsIcon}
- </div>
- `;
- },
- },
- [AutoComplete.MergeRequests]: {
- filterValues() {
- return this[AutoComplete.MergeRequests];
- },
- menuItemTemplate({ original }) {
- return `<small>${original.reference || original.iid}</small> ${escape(original.title)}`;
- },
- },
- [AutoComplete.Milestones]: {
- filterValues() {
- return this[AutoComplete.Milestones];
- },
- menuItemTemplate({ original }) {
- return escape(original.title);
- },
- },
- [AutoComplete.Snippets]: {
- filterValues() {
- return this[AutoComplete.Snippets];
- },
- menuItemTemplate({ original }) {
- return `<small>${original.id}</small> ${escape(original.title)}`;
- },
- },
-};
-
-export default {
- name: 'GlMentions',
- props: {
- dataSources: {
- type: Object,
- required: false,
- default: () => gl.GfmAutoComplete?.dataSources || {},
- },
- },
- mounted() {
- const NON_WORD_OR_INTEGER = /\W|^\d+$/;
-
- this.tribute = new Tribute({
- 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.type === groupType ? last(value.name.split(' / ')) : 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)
- ? `~"${escape(original.title)}"`
- : `~${escape(original.title)}`,
- values: this.getValues(AutoComplete.Labels),
- },
- {
- trigger: '!',
- lookup: value => value.iid + value.title,
- menuItemTemplate: autoCompleteMap[AutoComplete.MergeRequests].menuItemTemplate,
- selectTemplate: ({ original }) => original.reference || `!${original.iid}`,
- values: this.getValues(AutoComplete.MergeRequests),
- },
- {
- trigger: '%',
- lookup: 'title',
- menuItemTemplate: autoCompleteMap[AutoComplete.Milestones].menuItemTemplate,
- selectTemplate: ({ original }) => `%"${escape(original.title)}"`,
- values: this.getValues(AutoComplete.Milestones),
- },
- {
- trigger: '$',
- fillAttr: 'id',
- lookup: value => value.id + value.title,
- menuItemTemplate: autoCompleteMap[AutoComplete.Snippets].menuItemTemplate,
- values: this.getValues(AutoComplete.Snippets),
- },
- ],
- });
-
- const input = this.$slots.default?.[0]?.elm;
- this.tribute.attach(input);
- },
- beforeDestroy() {
- const input = this.$slots.default?.[0]?.elm;
- this.tribute.detach(input);
- },
- methods: {
- 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) {
- return createElement('div', this.$slots.default);
- },
-};
-</script>
diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js
index 02f28da8bb0..61ab2a698ce 100644
--- a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js
@@ -1,5 +1,3 @@
export function pixeliseValue(val) {
return val ? `${val}px` : '';
}
-
-export default {};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 9cfba85e0d8..0d703545073 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -10,14 +10,14 @@ import { deprecatedCreateFlash as Flash } from '~/flash';
import GLForm from '~/gl_form';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
-import GlMentions from '~/vue_shared/components/gl_mentions.vue';
+import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import axios from '~/lib/utils/axios_utils';
export default {
components: {
- GlMentions,
+ GfmAutocomplete,
MarkdownHeader,
MarkdownToolbar,
GlIcon,
@@ -246,9 +246,9 @@ export default {
/>
<div v-show="!previewMarkdown" class="md-write-holder">
<div class="zen-backdrop">
- <gl-mentions v-if="glFeatures.tributeAutocomplete">
+ <gfm-autocomplete v-if="glFeatures.tributeAutocomplete">
<slot name="textarea"></slot>
- </gl-mentions>
+ </gfm-autocomplete>
<slot v-else name="textarea"></slot>
<a
class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index 8104d919bf6..85481f3f7b4 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -1,10 +1,14 @@
<script>
import Pikaday from 'pikaday';
+import { GlIcon } from '@gitlab/ui';
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
export default {
name: 'DatePicker',
+ components: {
+ GlIcon,
+ },
props: {
label: {
type: String,
@@ -66,7 +70,7 @@ export default {
<div class="dropdown open">
<button type="button" class="dropdown-menu-toggle" data-toggle="dropdown" @click="toggled">
<span class="dropdown-toggle-text"> {{ label }} </span>
- <i class="fa fa-chevron-down" aria-hidden="true"> </i>
+ <gl-icon name="chevron-down" class="gl-absolute gl-right-3 gl-top-3 gl-text-gray-500" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue
index c90bd4da6c2..3dbf0ccdfa9 100644
--- a/app/assets/javascripts/vue_shared/components/select2_select.vue
+++ b/app/assets/javascripts/vue_shared/components/select2_select.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
import 'select2';
+import { loadCSSFile } from '~/lib/utils/css_utils';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
@@ -20,10 +21,14 @@ export default {
},
mounted() {
- $(this.$refs.dropdownInput)
- .val(this.value)
- .select2(this.options)
- .on('change', event => this.$emit('input', event.target.value));
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $(this.$refs.dropdownInput)
+ .val(this.value)
+ .select2(this.options)
+ .on('change', event => this.$emit('input', event.target.value));
+ })
+ .catch(() => {});
},
beforeDestroy() {
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 2f71907f772..8ce624aa303 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
@@ -105,6 +105,11 @@ export default {
required: false,
default: __('Manage group labels'),
},
+ isEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -131,6 +136,11 @@ export default {
showDropdownContents(showDropdownContents) {
this.setContentIsOnViewport(showDropdownContents);
},
+ isEditing(newVal) {
+ if (newVal) {
+ this.toggleDropdownContents();
+ }
+ },
},
mounted() {
this.setInitialState({
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
index 579ad53e6db..b48dfa8b452 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue
@@ -1,6 +1,7 @@
<script>
import { isFunction } from 'lodash';
import tooltip from '../directives/tooltip';
+import { hasHorizontalOverflow } from '~/lib/utils/dom_utils';
export default {
directives: {
@@ -49,7 +50,7 @@ export default {
},
updateTooltip() {
const target = this.selectTarget();
- this.showTooltip = Boolean(target && target.scrollWidth > target.offsetWidth);
+ this.showTooltip = hasHorizontalOverflow(target);
},
},
};
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/constants.js b/app/assets/javascripts/vue_shared/security_reports/components/constants.js
new file mode 100644
index 00000000000..9b1cbfe218b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/components/constants.js
@@ -0,0 +1,8 @@
+export const SEVERITY_CLASS_NAME_MAP = {
+ critical: 'text-danger-800',
+ high: 'text-danger-600',
+ medium: 'text-warning-400',
+ low: 'text-warning-200',
+ info: 'text-primary-400',
+ unknown: 'text-secondary-400',
+};
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue
new file mode 100644
index 00000000000..babb9fddcf6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/components/security_summary.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { SEVERITY_CLASS_NAME_MAP } from './constants';
+
+export default {
+ components: {
+ GlSprintf,
+ },
+ props: {
+ message: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ shouldShowCountMessage() {
+ return !this.message.status && Boolean(this.message.countMessage);
+ },
+ },
+ methods: {
+ getSeverityClass(severity) {
+ return SEVERITY_CLASS_NAME_MAP[severity];
+ },
+ },
+ slotNames: ['critical', 'high', 'other'],
+ spacingClasses: {
+ critical: 'gl-pl-4',
+ high: 'gl-px-2',
+ other: 'gl-px-2',
+ },
+};
+</script>
+
+<template>
+ <span>
+ <gl-sprintf :message="message.message">
+ <template #total="{content}">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ <span v-if="shouldShowCountMessage" class="gl-font-sm">
+ <gl-sprintf :message="message.countMessage">
+ <template v-for="slotName in $options.slotNames" #[slotName]="{content}">
+ <span :key="slotName">
+ <strong
+ v-if="message[slotName] > 0"
+ :class="[getSeverityClass(slotName), $options.spacingClasses[slotName]]"
+ >
+ {{ content }}
+ </strong>
+ <span v-else :class="$options.spacingClasses[slotName]">
+ {{ content }}
+ </span>
+ </span>
+ </template>
+ </gl-sprintf>
+ </span>
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index 2f87c4e7878..413b4a70b40 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -1,3 +1,9 @@
export const FEEDBACK_TYPE_DISMISSAL = 'dismissal';
export const FEEDBACK_TYPE_ISSUE = 'issue';
export const FEEDBACK_TYPE_MERGE_REQUEST = 'merge_request';
+
+/**
+ * Security scan report types, as provided by the backend.
+ */
+export const REPORT_TYPE_SAST = 'sast';
+export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
index 89253cc7116..b61783ed7b0 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -1,19 +1,28 @@
<script>
+import { mapActions, mapGetters } from 'vuex';
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue';
-import { status } from '~/reports/constants';
+import { LOADING, ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
import { s__ } from '~/locale';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
-import Flash from '~/flash';
+import createFlash from '~/flash';
import Api from '~/api';
+import SecuritySummary from './components/security_summary.vue';
+import store from './store';
+import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
+import { REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION } from './constants';
export default {
+ store,
components: {
GlIcon,
GlLink,
GlSprintf,
ReportSection,
+ SecuritySummary,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
pipelineId: {
type: Number,
@@ -27,25 +36,53 @@ export default {
type: String,
required: true,
},
+ sastComparisonPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ secretScanningComparisonPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
- hasSecurityReports: false,
+ availableSecurityReports: [],
+ canShowCounts: false,
- // Error state is shown even when successfully loaded, since success
+ // When core_security_mr_widget_counts is not enabled, the
+ // error state is shown even when successfully loaded, since success
// state suggests that the security scans detected no security problems,
// which is not necessarily the case. A future iteration will actually
// check whether problems were found and display the appropriate status.
- status: status.ERROR,
+ status: ERROR,
};
},
+ computed: {
+ ...mapGetters(['groupedSummaryText', 'summaryStatus']),
+ hasSecurityReports() {
+ return this.availableSecurityReports.length > 0;
+ },
+ hasSastReports() {
+ return this.availableSecurityReports.includes(REPORT_TYPE_SAST);
+ },
+ hasSecretDetectionReports() {
+ return this.availableSecurityReports.includes(REPORT_TYPE_SECRET_DETECTION);
+ },
+ isLoaded() {
+ return this.summaryStatus !== LOADING;
+ },
+ },
created() {
- this.checkHasSecurityReports(this.$options.reportTypes)
- .then(hasSecurityReports => {
- this.hasSecurityReports = hasSecurityReports;
+ this.checkAvailableSecurityReports(this.$options.reportTypes)
+ .then(availableSecurityReports => {
+ this.availableSecurityReports = Array.from(availableSecurityReports);
+ this.fetchCounts();
})
.catch(error => {
- Flash({
+ createFlash({
message: this.$options.i18n.apiError,
captureError: true,
error,
@@ -53,7 +90,18 @@ export default {
});
},
methods: {
- async checkHasSecurityReports(reportTypes) {
+ ...mapActions(MODULE_SAST, {
+ setSastDiffEndpoint: 'setDiffEndpoint',
+ fetchSastDiff: 'fetchDiff',
+ }),
+ ...mapActions(MODULE_SECRET_DETECTION, {
+ setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
+ fetchSecretDetectionDiff: 'fetchDiff',
+ }),
+ async checkAvailableSecurityReports(reportTypes) {
+ const reportTypesSet = new Set(reportTypes);
+ const availableReportTypes = new Set();
+
let page = 1;
while (page) {
// eslint-disable-next-line no-await-in-loop
@@ -62,18 +110,40 @@ export default {
page,
});
- const hasSecurityReports = jobs.some(({ artifacts = [] }) =>
- artifacts.some(({ file_type }) => reportTypes.includes(file_type)),
- );
+ jobs.forEach(({ artifacts = [] }) => {
+ artifacts.forEach(({ file_type }) => {
+ if (reportTypesSet.has(file_type)) {
+ availableReportTypes.add(file_type);
+ }
+ });
+ });
- if (hasSecurityReports) {
- return true;
+ // If we've found artifacts for all the report types, stop looking!
+ if (availableReportTypes.size === reportTypesSet.size) {
+ return availableReportTypes;
}
page = parseIntPagination(normalizeHeaders(headers)).nextPage;
}
- return false;
+ return availableReportTypes;
+ },
+ fetchCounts() {
+ if (!this.glFeatures.coreSecurityMrWidgetCounts) {
+ return;
+ }
+
+ if (this.sastComparisonPath && this.hasSastReports) {
+ this.setSastDiffEndpoint(this.sastComparisonPath);
+ this.fetchSastDiff();
+ this.canShowCounts = true;
+ }
+
+ if (this.secretScanningComparisonPath && this.hasSecretDetectionReports) {
+ this.setSecretDetectionDiffEndpoint(this.secretScanningComparisonPath);
+ this.fetchSecretDetectionDiff();
+ this.canShowCounts = true;
+ }
},
activatePipelinesTab() {
if (window.mrTabs) {
@@ -81,7 +151,7 @@ export default {
}
},
},
- reportTypes: ['sast', 'secret_detection'],
+ reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
i18n: {
apiError: s__(
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
@@ -89,13 +159,57 @@ export default {
scansHaveRun: s__(
'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
+ downloadFromPipelineTab: s__(
+ 'SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
+ ),
securityReportsHelp: s__('SecurityReports|Security reports help page link'),
},
+ summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
};
</script>
<template>
<report-section
- v-if="hasSecurityReports"
+ v-if="canShowCounts"
+ :status="summaryStatus"
+ :has-issues="false"
+ class="mr-widget-border-top mr-report"
+ data-testid="security-mr-widget"
+ >
+ <template v-for="slot in $options.summarySlots" #[slot]>
+ <span :key="slot">
+ <security-summary :message="groupedSummaryText" />
+
+ <gl-link
+ target="_blank"
+ data-testid="help"
+ :href="securityReportsDocsPath"
+ :aria-label="$options.i18n.securityReportsHelp"
+ >
+ <gl-icon name="question" />
+ </gl-link>
+ </span>
+ </template>
+
+ <template v-if="isLoaded" #sub-heading>
+ <span class="gl-font-sm">
+ <gl-sprintf :message="$options.i18n.downloadFromPipelineTab">
+ <template #link="{ content }">
+ <gl-link
+ class="gl-font-sm"
+ data-testid="show-pipelines"
+ @click="activatePipelinesTab"
+ >{{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </report-section>
+
+ <!-- TODO: Remove this section when removing core_security_mr_widget_counts
+ feature flag. See https://gitlab.com/gitlab-org/gitlab/-/issues/284097 -->
+ <report-section
+ v-else-if="hasSecurityReports"
:status="status"
:has-issues="false"
class="mr-widget-border-top mr-report"
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/constants.js b/app/assets/javascripts/vue_shared/security_reports/store/constants.js
new file mode 100644
index 00000000000..6aeab56eea2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/store/constants.js
@@ -0,0 +1,7 @@
+/**
+ * Vuex module names corresponding to security scan types. These are similar to
+ * the snake_case report types from the backend, but should not be considered
+ * to be equivalent.
+ */
+export const MODULE_SAST = 'sast';
+export const MODULE_SECRET_DETECTION = 'secretDetection';
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/getters.js b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
new file mode 100644
index 00000000000..1e5a60c32fd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
@@ -0,0 +1,66 @@
+import { s__, sprintf } from '~/locale';
+import { countVulnerabilities, groupedTextBuilder } from './utils';
+import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
+import { TRANSLATION_IS_LOADING } from './messages';
+
+export const summaryCounts = state =>
+ countVulnerabilities(
+ state.reportTypes.reduce((acc, reportType) => {
+ acc.push(...state[reportType].newIssues);
+ return acc;
+ }, []),
+ );
+
+export const groupedSummaryText = (state, getters) => {
+ const reportType = s__('ciReport|Security scanning');
+ let status = '';
+
+ // All reports are loading
+ if (getters.areAllReportsLoading) {
+ return { message: sprintf(TRANSLATION_IS_LOADING, { reportType }) };
+ }
+
+ // All reports returned error
+ if (getters.allReportsHaveError) {
+ return { message: s__('ciReport|Security scanning failed loading any results') };
+ }
+
+ if (getters.areReportsLoading && getters.anyReportHasError) {
+ status = s__('ciReport|is loading, errors when loading results');
+ } else if (getters.areReportsLoading && !getters.anyReportHasError) {
+ status = s__('ciReport|is loading');
+ } else if (!getters.areReportsLoading && getters.anyReportHasError) {
+ status = s__('ciReport|: Loading resulted in an error');
+ }
+
+ const { critical, high, other } = getters.summaryCounts;
+
+ return groupedTextBuilder({ reportType, status, critical, high, other });
+};
+
+export const summaryStatus = (state, getters) => {
+ if (getters.areReportsLoading) {
+ return LOADING;
+ }
+
+ if (getters.anyReportHasError || getters.anyReportHasIssues) {
+ return ERROR;
+ }
+
+ return SUCCESS;
+};
+
+export const areReportsLoading = state =>
+ state.reportTypes.some(reportType => state[reportType].isLoading);
+
+export const areAllReportsLoading = state =>
+ state.reportTypes.every(reportType => state[reportType].isLoading);
+
+export const allReportsHaveError = state =>
+ state.reportTypes.every(reportType => state[reportType].hasError);
+
+export const anyReportHasError = state =>
+ state.reportTypes.some(reportType => state[reportType].hasError);
+
+export const anyReportHasIssues = state =>
+ state.reportTypes.some(reportType => state[reportType].newIssues.length > 0);
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/index.js b/app/assets/javascripts/vue_shared/security_reports/store/index.js
new file mode 100644
index 00000000000..10705e04a21
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/store/index.js
@@ -0,0 +1,16 @@
+import Vuex from 'vuex';
+import * as getters from './getters';
+import state from './state';
+import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
+import sast from './modules/sast';
+import secretDetection from './modules/secret_detection';
+
+export default () =>
+ new Vuex.Store({
+ modules: {
+ [MODULE_SAST]: sast,
+ [MODULE_SECRET_DETECTION]: secretDetection,
+ },
+ getters,
+ state,
+ });
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/messages.js b/app/assets/javascripts/vue_shared/security_reports/store/messages.js
new file mode 100644
index 00000000000..c25e252a768
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/store/messages.js
@@ -0,0 +1,4 @@
+import { s__ } from '~/locale';
+
+export const TRANSLATION_IS_LOADING = s__('ciReport|%{reportType} is loading');
+export const TRANSLATION_HAS_ERROR = s__('ciReport|%{reportType}: Loading resulted in an error');
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/state.js b/app/assets/javascripts/vue_shared/security_reports/store/state.js
new file mode 100644
index 00000000000..5dc4d1ad2fb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/security_reports/store/state.js
@@ -0,0 +1,5 @@
+import { MODULE_SAST, MODULE_SECRET_DETECTION } from './constants';
+
+export default () => ({
+ reportTypes: [MODULE_SAST, MODULE_SECRET_DETECTION],
+});
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
index 6e50efae741..c5e786c92b1 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
@@ -1,5 +1,7 @@
import pollUntilComplete from '~/lib/utils/poll_until_complete';
import axios from '~/lib/utils/axios_utils';
+import { __, n__, sprintf } from '~/locale';
+import { CRITICAL, HIGH } from '~/vulnerabilities/constants';
import {
FEEDBACK_TYPE_DISMISSAL,
FEEDBACK_TYPE_ISSUE,
@@ -73,3 +75,79 @@ export const parseDiff = (diff, enrichData) => {
existing: diff.existing ? diff.existing.map(enrichVulnerability) : [],
};
};
+
+const createCountMessage = ({ critical, high, other, total }) => {
+ const otherMessage = n__('%d Other', '%d Others', other);
+ const countMessage = __(
+ '%{criticalStart}%{critical} Critical%{criticalEnd} %{highStart}%{high} High%{highEnd} and %{otherStart}%{otherMessage}%{otherEnd}',
+ );
+ return total ? sprintf(countMessage, { critical, high, otherMessage }) : '';
+};
+
+const createStatusMessage = ({ reportType, status, total }) => {
+ const vulnMessage = n__('vulnerability', 'vulnerabilities', total);
+ let message;
+ if (status) {
+ message = __('%{reportType} %{status}');
+ } else if (!total) {
+ message = __('%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities.');
+ } else {
+ message = __(
+ '%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}',
+ );
+ }
+ return sprintf(message, { reportType, status, total, vulnMessage });
+};
+
+/**
+ * Counts vulnerabilities.
+ * Returns the amount of critical, high, and other vulnerabilities.
+ *
+ * @param {Array} vulnerabilities The raw vulnerabilities to parse
+ * @returns {{critical: number, high: number, other: number}}
+ */
+export const countVulnerabilities = (vulnerabilities = []) =>
+ vulnerabilities.reduce(
+ (acc, { severity }) => {
+ if (severity === CRITICAL) {
+ acc.critical += 1;
+ } else if (severity === HIGH) {
+ acc.high += 1;
+ } else {
+ acc.other += 1;
+ }
+
+ return acc;
+ },
+ { critical: 0, high: 0, other: 0 },
+ );
+
+/**
+ * Takes an object of options and returns the object with an externalized string representing
+ * the critical, high, and other severity vulnerabilities for a given report.
+ *
+ * The resulting string _may_ still contain sprintf-style placeholders. These
+ * are left in place so they can be replaced with markup, via the
+ * SecuritySummary component.
+ * @param {{reportType: string, status: string, critical: number, high: number, other: number}} options
+ * @returns {Object} the parameters with an externalized string
+ */
+export const groupedTextBuilder = ({
+ reportType = '',
+ status = '',
+ critical = 0,
+ high = 0,
+ other = 0,
+} = {}) => {
+ const total = critical + high + other;
+
+ return {
+ countMessage: createCountMessage({ critical, high, other, total }),
+ message: createStatusMessage({ reportType, status, total }),
+ critical,
+ high,
+ other,
+ status,
+ total,
+ };
+};
diff --git a/app/assets/javascripts/vuex_shared/modules/members/index.js b/app/assets/javascripts/vuex_shared/modules/members/index.js
deleted file mode 100644
index 586d52a5288..00000000000
--- a/app/assets/javascripts/vuex_shared/modules/members/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import createState from 'ee_else_ce/vuex_shared/modules/members/state';
-import mutations from 'ee_else_ce/vuex_shared/modules/members/mutations';
-import * as actions from 'ee_else_ce/vuex_shared/modules/members/actions';
-
-export default initialState => ({
- namespaced: true,
- state: createState(initialState),
- actions,
- mutations,
-});
diff --git a/app/assets/javascripts/vulnerabilities/constants.js b/app/assets/javascripts/vulnerabilities/constants.js
new file mode 100644
index 00000000000..42fb38e8e7e
--- /dev/null
+++ b/app/assets/javascripts/vulnerabilities/constants.js
@@ -0,0 +1,15 @@
+/**
+ * Vulnerability severities as provided by the backend on vulnerability
+ * objects.
+ */
+export const CRITICAL = 'critical';
+export const HIGH = 'high';
+export const MEDIUM = 'medium';
+export const LOW = 'low';
+export const INFO = 'info';
+export const UNKNOWN = 'unknown';
+
+/**
+ * All vulnerability severities in decreasing order.
+ */
+export const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW, INFO, UNKNOWN];
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 4b1139d2354..85a7fa1d2b1 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -5,7 +5,6 @@
// directory.
@import '@gitlab/at.js/dist/css/jquery.atwho';
@import 'dropzone/dist/basic';
-@import 'select2';
// GitLab UI framework
@import 'framework';
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 4f09f1a394b..d9ad4992458 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -253,3 +253,111 @@
vertical-align: middle;
}
}
+
+
+// The following encompasses the "add reaction" button redesign to
+// align properly within GitLab UI's gl-button. The implementation
+// above will be deprecated once all instances of "award emoji" are
+// migrated to Vue.
+
+.gl-button .award-emoji-block gl-emoji {
+ top: -1px;
+ margin-top: -1px;
+ margin-bottom: -1px;
+}
+
+.add-reaction-button {
+ position: relative;
+
+ // This forces the height and width of the inner content to match
+ // other gl-buttons despite all child elements being set to
+ // `position:absolute`
+ &::after {
+ content: '\a0';
+ width: 1em;
+ }
+
+ .reaction-control-icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+
+ // center the icon vertically and horizontally within the button
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ @include transition(opacity, transform);
+
+ .gl-icon {
+ height: $default-icon-size;
+ width: $default-icon-size;
+ }
+ }
+
+ .reaction-control-icon-neutral {
+ opacity: 1;
+ }
+
+ .reaction-control-icon-positive,
+ .reaction-control-icon-super-positive {
+ opacity: 0;
+ }
+
+ &:hover,
+ &.active,
+ &:active,
+ &.is-active {
+ // extra specificty added to override another selector
+ .reaction-control-icon .gl-icon {
+ color: $blue-500;
+ transform: scale(1.15);
+ }
+
+ .reaction-control-icon-neutral {
+ opacity: 0;
+ }
+ }
+
+ &:hover {
+ .reaction-control-icon-positive {
+ opacity: 1;
+ }
+ }
+
+ &.active,
+ &:active,
+ &.is-active {
+ .reaction-control-icon-positive {
+ opacity: 0;
+ }
+
+ .reaction-control-icon-super-positive {
+ opacity: 1;
+ }
+ }
+
+ &.disabled {
+ cursor: default;
+
+ &:hover,
+ &:focus,
+ &:active {
+ .reaction-control-icon .gl-icon {
+ color: inherit;
+ transform: scale(1);
+ }
+
+ .reaction-control-icon-neutral {
+ opacity: 1;
+ }
+
+ .reaction-control-icon-positive,
+ .reaction-control-icon-super-positive {
+ opacity: 0;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index deb2d6c4641..3b59c028437 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -135,7 +135,6 @@ hr {
text-overflow: ellipsis;
white-space: nowrap;
- > div:not(.block):not(.select2-display-none),
.str-truncated {
display: inline;
}
@@ -389,11 +388,7 @@ 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-15 { margin-top: 15px; }
.prepend-top-20 { margin-top: 20px; }
-.prepend-left-15 { margin-left: 15px; }
-.prepend-left-20 { margin-left: 20px; }
-.append-right-20 { margin-right: 20px; }
.append-bottom-20 { margin-bottom: 20px; }
.ml-10 { margin-left: 4.5rem; }
.inline { display: inline-block; }
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index e16ab5ee72f..a9925fb3621 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -2,10 +2,6 @@
.diff-file {
margin-bottom: $gl-padding;
- &.conflict {
- border-top: 1px solid $border-color;
- }
-
&.has-body {
.file-title {
box-shadow: 0 -2px 0 0 var(--white);
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 7be676ed83c..be97db42d1d 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -133,11 +133,6 @@ label {
}
.input-group {
- .select2-container {
- display: table-cell;
- max-width: 180px;
- }
-
.input-group-prepend,
.input-group-append {
background-color: $input-group-addon-bg;
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 86a5aa1a16e..d8ce6826fc1 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -1,275 +1,3 @@
-/** Select2 selectbox style override **/
-.select2-container {
- width: 100% !important;
-
- &.input-md,
- &.input-lg {
- display: block;
- }
-}
-
-.select2-container,
-.select2-container.select2-drop-above {
- .select2-choice {
- background: $white;
- color: $gl-text-color;
- border-color: $input-border;
- height: 34px;
- padding: $gl-vert-padding $gl-input-padding;
- font-size: $gl-font-size;
- line-height: 1.42857143;
- border-radius: $border-radius-base;
-
- .select2-arrow {
- background-image: none;
- background-color: transparent;
- border: 0;
- padding-top: 12px;
- padding-right: 20px;
- font-size: 10px;
-
- b {
- display: none;
- }
-
- &::after {
- content: '\f078';
- position: absolute;
- z-index: 1;
- text-align: center;
- pointer-events: none;
- box-sizing: border-box;
- color: $gray-darkest;
- 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;
- }
- }
-
- .select2-chosen {
- margin-right: 15px;
- }
-
- &:hover {
- border-color: $gray-darkest;
- color: $gl-text-color;
- }
- }
-
- // Essentially we’re doing @include form-control-focus here (from
- // bootstrap/scss/mixins/_forms.scss), except that the bootstrap mixin adds a
- // `&:focus` selector and we’re never actually focusing the .select2-choice
- // link nor the .select2-container, the Select2 library focuses an off-screen
- // .select2-focusser element instead.
- &.select2-container-active:not(.select2-dropdown-open) {
- .select2-choice {
- color: $input-focus-color;
- background-color: $input-focus-bg;
- border-color: $input-focus-border-color;
- outline: 0;
- }
-
- // Reusable focus “glow” box-shadow
- @mixin form-control-focus-glow {
- @if $enable-shadows {
- box-shadow: $input-box-shadow, $input-focus-box-shadow;
- } @else {
- box-shadow: $input-focus-box-shadow;
- }
- }
-
- // Apply the focus “glow” shadow to the .select2-container if it also has
- // the .block-truncated class as that applies an overflow: hidden, thereby
- // hiding the glow of the nested .select2-choice element.
- &.block-truncated {
- @include form-control-focus-glow;
- }
-
- // Apply the glow directly to the .select2-choice link if we’re not
- // block-truncating the container.
- &:not(.block-truncated) .select2-choice {
- @include form-control-focus-glow;
- }
- }
-
- &.is-invalid {
- ~ .invalid-feedback {
- display: block;
- }
-
- .select2-choices,
- .select2-choice {
- border-color: $red-500;
- }
- }
-}
-
-.select2-drop,
-.select2-drop.select2-drop-above {
- background: $white;
- box-shadow: 0 2px 4px $dropdown-shadow-color;
- border-radius: $border-radius-base;
- border: 1px solid $border-color;
- min-width: 175px;
- color: $gl-text-color;
- z-index: 999;
-
- .modal-open & {
- z-index: $zindex-modal + 200;
- }
-}
-
-.select2-drop-mask {
- z-index: 998;
-
- .modal-open & {
- z-index: $zindex-modal + 100;
- }
-}
-
-.select2-drop.select2-drop-above.select2-drop-active {
- border-top: 1px solid $border-color;
- margin-top: -6px;
-}
-
-.select2-container-active {
- .select2-choice,
- .select2-choices {
- box-shadow: none;
- }
-}
-
-.select2-dropdown-open,
-.select2-dropdown-open.select2-drop-above {
- .select2-choice {
- border-color: $gray-darkest;
- outline: 0;
- }
-}
-
-.select2-container-multi {
- .select2-choices {
- border-radius: $border-radius-default;
- border-color: $input-border;
- background: none;
-
- .select2-search-field input {
- padding: 5px $gl-input-padding;
- height: auto;
- font-family: inherit;
- font-size: inherit;
- }
-
- .select2-search-choice {
- margin: 5px 0 0 8px;
- box-shadow: none;
- border-color: $input-border;
- color: $gl-text-color;
- line-height: 15px;
- background-color: $gray-light;
- background-image: none;
- padding: 3px 18px 3px 5px;
-
- .select2-search-choice-close {
- top: 5px;
- left: initial;
- right: 3px;
- }
-
- &.select2-search-choice-focus {
- border-color: $gl-text-color;
- }
- }
- }
-}
-
-.select2-drop-active {
- margin-top: $dropdown-vertical-offset;
- font-size: 14px;
-
- .select2-results {
- max-height: 350px;
- }
-}
-
-.select2-search {
- padding: $grid-size;
-
- .select2-drop-auto-width & {
- padding: $grid-size;
- }
-
- input {
- padding: $grid-size;
- background: transparent image-url('select2.png');
- color: $gl-text-color;
- background-clip: content-box;
- background-origin: content-box;
- background-repeat: no-repeat;
- background-position: right 0 bottom 0 !important;
- border: 1px solid $input-border;
- border-radius: $border-radius-default;
- line-height: 16px;
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
-
- &:focus {
- border-color: $blue-300;
- }
-
- &.select2-active {
- background-color: $white;
- background-image: image-url('select2-spinner.gif') !important;
- background-origin: content-box;
- background-repeat: no-repeat;
- background-position: right 6px center !important;
- background-size: 16px 16px !important;
- }
- }
-
- + .select2-results {
- padding-top: 0;
- }
-}
-
-.select2-results {
- margin: 0;
- padding: #{$gl-padding / 2} 0;
-
- .select2-no-results,
- .select2-searching,
- .select2-ajax-error,
- .select2-selection-limit {
- background: transparent;
- padding: #{$gl-padding / 2} $gl-padding;
- }
-
- .select2-result-label,
- .select2-more-results {
- padding: #{$gl-padding / 2} $gl-padding;
- }
-
- .select2-highlighted {
- background: transparent;
- color: $gl-text-color;
-
- .select2-result-label {
- background: $gray-darker;
- }
- }
-
- .select2-result {
- padding: 0 1px;
- }
-
- li.select2-result-with-children > .select2-result-label {
- font-weight: $gl-font-weight-bold;
- color: $gl-text-color;
- }
-}
-
.ajax-users-select {
width: 400px;
@@ -282,14 +10,6 @@
}
}
-.select2-highlighted {
- .group-result {
- .group-path {
- color: $gray-700;
- }
- }
-}
-
.group-result {
.group-image {
float: left;
@@ -345,11 +65,3 @@
.ajax-users-dropdown {
min-width: 250px !important;
}
-
-.select2-result-selectable,
-.select2-result-unselectable {
- .select2-match {
- font-weight: $gl-font-weight-bold;
- text-decoration: none;
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index ffc15af6329..a88ca474bb7 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -92,7 +92,6 @@
.board-title-caret {
border-radius: $border-radius-default;
line-height: $gl-spacing-scale-5;
- height: $gl-spacing-scale-5;
&.btn svg {
top: 0;
diff --git a/app/assets/stylesheets/page_bundles/ci_status.scss b/app/assets/stylesheets/page_bundles/ci_status.scss
index 8522a0a8fe4..89fbe1c4caf 100644
--- a/app/assets/stylesheets/page_bundles/ci_status.scss
+++ b/app/assets/stylesheets/page_bundles/ci_status.scss
@@ -26,6 +26,7 @@
}
&.ci-canceled,
+ &.ci-skipped,
&.ci-disabled,
&.ci-scheduled,
&.ci-manual {
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 4e27f438e36..f7b8a4c5b84 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -58,22 +58,6 @@
}
}
-.cluster-application-banner {
- height: 45px;
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.cluster-application-banner-close {
- align-self: flex-start;
- font-weight: 500;
- font-size: 20px;
- color: $orange-500;
- opacity: 1;
- margin: $gl-padding-8 14px 0 0;
-}
-
.cluster-application-description {
flex: 1;
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 5c845c37e90..acf9a654159 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -74,10 +74,6 @@
justify-content: flex-end;
}
- .select2 {
- float: right;
- }
-
.encoding-selector,
.soft-wrap-toggle {
display: inline-block;
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index 74f80a11471..3f40d2c433d 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -17,14 +17,6 @@
max-width: 300px;
}
-.import-namespace-select {
- > .select2-choice {
- border-radius: $border-radius-default 0 0 $border-radius-default;
- position: relative;
- left: 1px;
- }
-}
-
.import-slash-divider {
background-color: $gray-lightest;
border: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index cc4827f75d4..aa849e1b17b 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -199,10 +199,6 @@
border: 0;
}
- .select2-container span {
- margin-top: 0;
- }
-
&.assignee {
.author-link {
display: block;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 08faebc8ec0..51870ace23b 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -92,6 +92,11 @@ ul.related-merge-requests > li {
}
}
+.issues-footer {
+ padding-top: $gl-padding;
+ padding-bottom: 37px;
+}
+
.issues-nav-controls,
.new-branch-col {
font-size: 0;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 09501d3713d..e5a9e99b2fb 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -10,12 +10,6 @@
}
.input-group {
- .select2-container {
- display: unset;
- max-width: unset;
- flex-grow: 1;
- }
-
> div {
&:last-child {
padding-right: 0;
@@ -52,7 +46,6 @@
flex-grow: 1;
}
- + .select2 a,
+ .btn-default {
border-radius: 0 $border-radius-base $border-radius-base 0;
}
@@ -258,10 +251,6 @@
color: $gray-700;
}
-.transfer-project .select2-container {
- min-width: 200px;
-}
-
.deploy-key {
// Ensure that the fingerprint does not overflow on small screens
.fingerprint {
@@ -1057,11 +1046,6 @@ pre.light-well {
margin-bottom: 0;
}
}
-
- .select2-choice {
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
}
.project-home-empty {
diff --git a/app/controllers/admin/cohorts_controller.rb b/app/controllers/admin/cohorts_controller.rb
index d5cd9c55422..a26dc554506 100644
--- a/app/controllers/admin/cohorts_controller.rb
+++ b/app/controllers/admin/cohorts_controller.rb
@@ -5,7 +5,7 @@ class Admin::CohortsController < Admin::ApplicationController
track_unique_visits :index, target_id: 'i_analytics_cohorts'
- feature_category :instance_statistics
+ feature_category :devops_reports
def index
if Gitlab::CurrentSettings.usage_ping_enabled
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 33a8cc4ae42..da89276f5eb 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -2,7 +2,6 @@
class Admin::DashboardController < Admin::ApplicationController
include CountHelper
- helper_method :show_license_breakdown?
COUNTED_ITEMS = [Project, User, Group].freeze
@@ -23,10 +22,6 @@ class Admin::DashboardController < Admin::ApplicationController
def stats
@users_statistics = UsersStatistics.latest
end
-
- def show_license_breakdown?
- false
- end
end
Admin::DashboardController.prepend_if_ee('EE::Admin::DashboardController')
diff --git a/app/controllers/admin/instance_review_controller.rb b/app/controllers/admin/instance_review_controller.rb
index db304c82dd6..88ca2c88aab 100644
--- a/app/controllers/admin/instance_review_controller.rb
+++ b/app/controllers/admin/instance_review_controller.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
class Admin::InstanceReviewController < Admin::ApplicationController
- feature_category :instance_statistics
+ feature_category :devops_reports
def index
redirect_to("#{::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL}/instance_review?#{instance_review_params}")
diff --git a/app/controllers/admin/instance_statistics_controller.rb b/app/controllers/admin/instance_statistics_controller.rb
index 05a0a1ce314..30891fcfe7c 100644
--- a/app/controllers/admin/instance_statistics_controller.rb
+++ b/app/controllers/admin/instance_statistics_controller.rb
@@ -7,7 +7,7 @@ class Admin::InstanceStatisticsController < Admin::ApplicationController
track_unique_visits :index, target_id: 'i_analytics_instance_statistics'
- feature_category :instance_statistics
+ feature_category :devops_reports
def index
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 0d7af57328a..3f5f3b6e9df 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -150,7 +150,7 @@ module IssuableCollections
common_attributes + [:project, project: :namespace]
when 'MergeRequest'
common_attributes + [
- :target_project, :latest_merge_request_diff, :approvals, :approved_by_users,
+ :target_project, :latest_merge_request_diff, :approvals, :approved_by_users, :reviewers,
source_project: :route, head_pipeline: :project, target_project: :namespace
]
end
diff --git a/app/controllers/concerns/sorting_preference.rb b/app/controllers/concerns/sorting_preference.rb
index a51b68147d5..8d8845e2f41 100644
--- a/app/controllers/concerns/sorting_preference.rb
+++ b/app/controllers/concerns/sorting_preference.rb
@@ -4,8 +4,11 @@ module SortingPreference
include SortingHelper
include CookiesHelper
- def set_sort_order
- set_sort_order_from_user_preference || set_sort_order_from_cookie || params[:sort] || default_sort_order
+ def set_sort_order(field = sorting_field, default_order = default_sort_order)
+ set_sort_order_from_user_preference(field) ||
+ set_sort_order_from_cookie(field) ||
+ params[:sort] ||
+ default_order
end
# Implement sorting_field method on controllers
@@ -29,42 +32,42 @@ module SortingPreference
private
- def set_sort_order_from_user_preference
+ def set_sort_order_from_user_preference(field = sorting_field)
return unless current_user
- return unless sorting_field
+ return unless field
user_preference = current_user.user_preference
sort_param = params[:sort]
- sort_param ||= user_preference[sorting_field]
+ sort_param ||= user_preference[field]
return sort_param if Gitlab::Database.read_only?
- if user_preference[sorting_field] != sort_param
- user_preference.update(sorting_field => sort_param)
+ if user_preference[field] != sort_param
+ user_preference.update(field => sort_param)
end
sort_param
end
- def set_sort_order_from_cookie
+ def set_sort_order_from_cookie(field = sorting_field)
return unless legacy_sort_cookie_name
sort_param = params[:sort] if params[:sort].present?
# fallback to legacy cookie value for backward compatibility
sort_param ||= cookies[legacy_sort_cookie_name]
- sort_param ||= cookies[remember_sorting_key]
+ sort_param ||= cookies[remember_sorting_key(field)]
sort_value = update_cookie_value(sort_param)
- set_secure_cookie(remember_sorting_key, sort_value)
+ set_secure_cookie(remember_sorting_key(field), sort_value)
sort_value
end
# Convert sorting_field to legacy cookie name for backwards compatibility
# :merge_requests_sort => 'mergerequest_sort'
# :issues_sort => 'issue_sort'
- def remember_sorting_key
- @remember_sorting_key ||= sorting_field
+ def remember_sorting_key(field = sorting_field)
+ @remember_sorting_key ||= field
.to_s
.split('_')[0..-2]
.map(&:singularize)
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 6abb2e16226..65ab835c33c 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -8,6 +8,8 @@ module WikiActions
include RedisTracking
extend ActiveSupport::Concern
+ RESCUE_GIT_TIMEOUTS_IN = %w[show edit history diff pages].freeze
+
included do
before_action { respond_to :html }
@@ -38,6 +40,12 @@ module WikiActions
feature: :track_unique_wiki_page_views, feature_default_enabled: true
helper_method :view_file_button, :diff_file_html_data
+
+ rescue_from ::Gitlab::Git::CommandTimedOut do |exc|
+ raise exc unless RESCUE_GIT_TIMEOUTS_IN.include?(action_name)
+
+ render 'shared/wikis/git_error'
+ end
end
def new
@@ -46,11 +54,7 @@ module WikiActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def pages
- @wiki_pages = Kaminari.paginate_array(
- wiki.list_pages(sort: params[:sort], direction: params[:direction])
- ).page(params[:page])
-
- @wiki_entries = WikiDirectory.group_pages(@wiki_pages)
+ @wiki_entries = WikiDirectory.group_pages(wiki_pages)
render 'shared/wikis/pages'
end
@@ -225,9 +229,19 @@ module WikiActions
unless @sidebar_page # Fallback to default sidebar
@sidebar_wiki_entries, @sidebar_limited = wiki.sidebar_entries
end
+ rescue ::Gitlab::Git::CommandTimedOut => e
+ @sidebar_error = e
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
+ def wiki_pages
+ strong_memoize(:wiki_pages) do
+ Kaminari.paginate_array(
+ wiki.list_pages(sort: params[:sort], direction: params[:direction])
+ ).page(params[:page])
+ end
+ end
+
def wiki_params
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 9c2e361e92f..a504d2ce991 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -3,11 +3,14 @@
class Groups::ApplicationController < ApplicationController
include RoutableActions
include ControllerWithCrossProjectAccessCheck
+ include SortingHelper
+ include SortingPreference
layout 'group'
skip_before_action :authenticate_user!
before_action :group
+ before_action :set_sorting
requires_cross_project_access
private
@@ -57,6 +60,16 @@ class Groups::ApplicationController < ApplicationController
url_for(safe_params)
end
+
+ def set_sorting
+ if has_project_list?
+ @group_projects_sort = set_sort_order(Project::SORTING_PREFERENCE_FIELD, sort_value_name)
+ end
+ end
+
+ def has_project_list?
+ false
+ end
end
Groups::ApplicationController.prepend_if_ee('EE::Groups::ApplicationController')
diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb
index 718914dea35..10a6ad06ae5 100644
--- a/app/controllers/groups/children_controller.rb
+++ b/app/controllers/groups/children_controller.rb
@@ -2,12 +2,15 @@
module Groups
class ChildrenController < Groups::ApplicationController
+ extend ::Gitlab::Utils::Override
+
before_action :group
skip_cross_project_access_check :index
feature_category :subgroups
def index
+ params[:sort] ||= @group_projects_sort
parent = if params[:parent_id].present?
GroupFinder.new(current_user).execute(id: params[:parent_id])
else
@@ -40,5 +43,12 @@ module Groups
params: params.to_unsafe_h).execute
@children = @children.page(params[:page])
end
+
+ private
+
+ override :has_project_list?
+ def has_project_list?
+ true
+ end
end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 03d41f1dd6d..84dc570a1e9 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -5,9 +5,6 @@ class Groups::MilestonesController < Groups::ApplicationController
before_action :milestone, only: [:edit, :show, :update, :issues, :merge_requests, :participants, :labels, :destroy]
before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update, :destroy]
- before_action do
- push_frontend_feature_flag(:burnup_charts, @group, default_enabled: true)
- end
feature_category :issue_tracking
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 8d528e123e1..40cb40c9905 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -329,6 +329,11 @@ class GroupsController < Groups::ApplicationController
def markdown_service_params
params.merge(group: group)
end
+
+ override :has_project_list?
+ def has_project_list?
+ %w(details show index).include?(action_name)
+ end
end
GroupsController.prepend_if_ee('EE::GroupsController')
diff --git a/app/controllers/projects/alert_management_controller.rb b/app/controllers/projects/alert_management_controller.rb
index 8ecf8fadefd..ebe867d915d 100644
--- a/app/controllers/projects/alert_management_controller.rb
+++ b/app/controllers/projects/alert_management_controller.rb
@@ -3,7 +3,7 @@
class Projects::AlertManagementController < Projects::ApplicationController
before_action :authorize_read_alert_management_alert!
- feature_category :alert_management
+ feature_category :incident_management
def index
end
diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb
index a3f4d784f25..86121bed381 100644
--- a/app/controllers/projects/alerting/notifications_controller.rb
+++ b/app/controllers/projects/alerting/notifications_controller.rb
@@ -10,7 +10,7 @@ module Projects
prepend_before_action :repository, :project_without_auth
- feature_category :alert_management
+ feature_category :incident_management
def create
token = extract_alert_manager_token(request)
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 02e941db636..d1c0f7edc5c 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -33,7 +33,6 @@ class Projects::BlobController < Projects::ApplicationController
before_action :set_last_commit_sha, only: [:edit, :update]
before_action only: :show do
- push_frontend_feature_flag(:suggest_pipeline, default_enabled: true)
push_frontend_feature_flag(:gitlab_ci_yml_preview, @project, default_enabled: false)
end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 7fbeac12644..da19ddf6105 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -69,7 +69,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
}
options = additional_attributes.merge(
- diff_view: unified_diff_lines_view_type(@merge_request.project),
+ diff_view: "inline",
merge_ref_head_diff: render_merge_ref_head_diff?
)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index f2b41294a85..627d578643f 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -27,7 +27,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
before_action only: [:show] do
- push_frontend_feature_flag(:suggest_pipeline, default_enabled: true)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true)
push_frontend_feature_flag(:multiline_comments, @project, default_enabled: true)
@@ -36,11 +35,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
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)
- push_frontend_feature_flag(:unified_diff_lines, @project, default_enabled: true)
push_frontend_feature_flag(:unified_diff_components, @project)
push_frontend_feature_flag(:highlight_current_diff_row, @project)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
+ push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
push_frontend_feature_flag(:test_failure_history, @project)
@@ -481,7 +480,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def endpoint_metadata_url(project, merge_request)
params = request.query_parameters
- params[:view] = unified_diff_lines_view_type(project)
+ params[:view] = "inline"
if Feature.enabled?(:default_merge_ref_for_diffs, project)
params = params.merge(diff_head: true)
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 31189c888b7..dcd3c49441e 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -6,9 +6,6 @@ class Projects::MilestonesController < Projects::ApplicationController
before_action :check_issuables_available!
before_action :milestone, only: [:edit, :update, :destroy, :show, :issues, :merge_requests, :participants, :labels, :promote]
- before_action do
- push_frontend_feature_flag(:burnup_charts, @project, default_enabled: true)
- end
# Allow read any milestone
before_action :authorize_read_milestone!
diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb
index 2892542e63c..7f711417f0b 100644
--- a/app/controllers/projects/prometheus/alerts_controller.rb
+++ b/app/controllers/projects/prometheus/alerts_controller.rb
@@ -16,7 +16,7 @@ module Projects
before_action :authorize_read_prometheus_alerts!, except: [:notify]
before_action :alert, only: [:update, :show, :destroy, :metrics_dashboard]
- feature_category :alert_management
+ feature_category :incident_management
def index
render json: serialize_as_json(alerts)
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index c9386a2edec..f8155b77e60 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -7,7 +7,6 @@ module Projects
before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token]
before_action do
- push_frontend_feature_flag(:http_integrations_list, @project)
push_frontend_feature_flag(:multiple_http_integrations_custom_mapping, @project)
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index c03a820b384..8666b0f9576 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -34,12 +34,6 @@ class ProjectsController < Projects::ApplicationController
# Project Export Rate Limit
before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export]
- # Experiments
- before_action only: [:new, :create] do
- frontend_experimentation_tracking_data(:new_create_project_ui, 'click_tab')
- push_frontend_experiment(:new_create_project_ui)
- end
-
before_action only: [:edit] do
push_frontend_feature_flag(:service_desk_custom_address, @project)
push_frontend_feature_flag(:approval_suggestions, @project, default_enabled: true)
diff --git a/app/controllers/repositories/lfs_api_controller.rb b/app/controllers/repositories/lfs_api_controller.rb
index 96185608c09..ab846966792 100644
--- a/app/controllers/repositories/lfs_api_controller.rb
+++ b/app/controllers/repositories/lfs_api_controller.rb
@@ -92,16 +92,26 @@ module Repositories
{
upload: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}",
- header: {
- Authorization: authorization_header,
- # git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This
- # ensures that Workhorse can intercept the request.
- 'Content-Type': LFS_TRANSFER_CONTENT_TYPE
- }.compact
+ header: upload_headers
}
}
end
+ def upload_headers
+ headers = {
+ Authorization: authorization_header,
+ # git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This
+ # ensures that Workhorse can intercept the request.
+ 'Content-Type': LFS_TRANSFER_CONTENT_TYPE
+ }
+
+ if Feature.enabled?(:lfs_chunked_encoding, project)
+ headers['Transfer-Encoding'] = 'chunked'
+ end
+
+ headers
+ end
+
def lfs_check_batch_operation!
if batch_operation_disallowed?
render(
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 6692c285335..2c827292928 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -27,6 +27,10 @@ class UploadsController < ApplicationController
feature_category :not_owned
+ def self.model_classes
+ MODEL_CLASSES
+ end
+
def uploader_class
PersonalFileUploader
end
@@ -99,7 +103,7 @@ class UploadsController < ApplicationController
end
def upload_model_class
- MODEL_CLASSES[params[:model]] || raise(UnknownUploadModelError)
+ self.class.model_classes[params[:model]] || raise(UnknownUploadModelError)
end
def upload_model_class_has_mounts?
@@ -112,3 +116,5 @@ class UploadsController < ApplicationController
upload_model_class.uploader_options.has_key?(upload_mount)
end
end
+
+UploadsController.prepend_if_ee('EE::UploadsController')
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index d431c3e3699..922b53b514d 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -339,15 +339,6 @@ class IssuableFinder
cte << items
items = klass.with(cte.to_arel).from(klass.table_name)
- elsif Feature.enabled?(:pg_hint_plan_for_issuables, params.project)
- items = items.optimizer_hints(<<~HINTS)
- BitmapScan(
- issues idx_issues_on_project_id_and_created_at_and_id_and_state_id
- idx_issues_on_project_id_and_due_date_and_id_and_state_id
- idx_issues_on_project_id_and_updated_at_and_id_and_state_id
- index_issues_on_project_id_and_iid
- )
- HINTS
end
items.full_search(search, matched_columns: params[:in], use_minimum_char_limit: !use_cte_for_search?)
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 1f847b09752..978550aedaf 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -41,6 +41,8 @@ class MergeRequestsFinder < IssuableFinder
:environment,
:merged_after,
:merged_before,
+ :reviewer_id,
+ :reviewer_username,
:target_branch,
:wip
]
@@ -54,6 +56,10 @@ class MergeRequestsFinder < IssuableFinder
MergeRequest
end
+ def params_class
+ MergeRequestsFinder::Params
+ end
+
def filter_items(_items)
items = by_commit(super)
items = by_source_branch(items)
@@ -62,12 +68,14 @@ class MergeRequestsFinder < IssuableFinder
items = by_merged_at(items)
items = by_approvals(items)
items = by_deployments(items)
+ items = by_reviewer(items)
by_source_project_id(items)
end
def filter_negated_items(items)
items = super(items)
+ items = by_negated_reviewer(items)
by_negated_target_branch(items)
end
@@ -186,6 +194,30 @@ class MergeRequestsFinder < IssuableFinder
items.where_exists(deploys)
end
+
+ def by_reviewer(items)
+ return items unless params.reviewer_id? || params.reviewer_username?
+
+ if params.filter_by_no_reviewer?
+ items.no_review_requested
+ elsif params.filter_by_any_reviewer?
+ items.review_requested
+ elsif params.reviewer
+ items.review_requested_to(params.reviewer)
+ else # reviewer not found
+ items.none
+ end
+ end
+
+ def by_negated_reviewer(items)
+ return items unless not_params.reviewer_id? || not_params.reviewer_username?
+
+ if not_params.reviewer.present?
+ items.no_review_requested_to(not_params.reviewer)
+ else # reviewer not found
+ items.none
+ end
+ end
end
MergeRequestsFinder.prepend_if_ee('EE::MergeRequestsFinder')
diff --git a/app/finders/merge_requests_finder/params.rb b/app/finders/merge_requests_finder/params.rb
new file mode 100644
index 00000000000..e44e96054d3
--- /dev/null
+++ b/app/finders/merge_requests_finder/params.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class MergeRequestsFinder
+ class Params < IssuableFinder::Params
+ def filter_by_no_reviewer?
+ params[:reviewer_id].to_s.downcase == FILTER_NONE
+ end
+
+ def filter_by_any_reviewer?
+ params[:reviewer_id].to_s.downcase == FILTER_ANY
+ end
+
+ def reviewer
+ strong_memoize(:reviewer) do
+ if reviewer_id?
+ User.find_by_id(params[:reviewer_id])
+ elsif reviewer_username?
+ User.find_by_username(params[:reviewer_username])
+ else
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/app/finders/releases/evidence_pipeline_finder.rb b/app/finders/releases/evidence_pipeline_finder.rb
new file mode 100644
index 00000000000..2e706087feb
--- /dev/null
+++ b/app/finders/releases/evidence_pipeline_finder.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Releases
+ class EvidencePipelineFinder
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :project, :params
+
+ def initialize(project, params = {})
+ @project = project
+ @params = params
+ end
+
+ def execute
+ # TODO: remove this with the release creation moved to it's own form https://gitlab.com/gitlab-org/gitlab/-/issues/214245
+ return params[:evidence_pipeline] if params[:evidence_pipeline]
+
+ sha = existing_tag&.dereferenced_target&.sha
+ sha ||= repository&.commit(ref)&.sha
+
+ return unless sha
+
+ project.ci_pipelines.for_sha(sha).last
+ end
+
+ private
+
+ def repository
+ strong_memoize(:repository) do
+ project.repository
+ end
+ end
+
+ def existing_tag
+ repository.find_tag(tag_name)
+ end
+
+ def tag_name
+ params[:tag]
+ end
+
+ def ref
+ params[:ref]
+ end
+ end
+end
diff --git a/app/graphql/mutations/alert_management/create_alert_issue.rb b/app/graphql/mutations/alert_management/create_alert_issue.rb
index 2ddb94700c2..2c128e1b339 100644
--- a/app/graphql/mutations/alert_management/create_alert_issue.rb
+++ b/app/graphql/mutations/alert_management/create_alert_issue.rb
@@ -10,6 +10,7 @@ module Mutations
result = create_alert_issue(alert, current_user)
track_usage_event(:incident_management_incident_created, current_user.id)
+ track_usage_event(:incident_management_alert_create_incident, current_user.id)
prepare_response(alert, result)
end
diff --git a/app/graphql/mutations/award_emojis/add.rb b/app/graphql/mutations/award_emojis/add.rb
index 856fdd5fb14..e7ee2ec4fad 100644
--- a/app/graphql/mutations/award_emojis/add.rb
+++ b/app/graphql/mutations/award_emojis/add.rb
@@ -8,8 +8,6 @@ module Mutations
def resolve(args)
awardable = authorized_find!(id: args[:awardable_id])
- check_object_is_awardable!(awardable)
-
service = ::AwardEmojis::AddService.new(awardable, args[:name], current_user).execute
{
diff --git a/app/graphql/mutations/award_emojis/base.rb b/app/graphql/mutations/award_emojis/base.rb
index df6b883529e..28140054dea 100644
--- a/app/graphql/mutations/award_emojis/base.rb
+++ b/app/graphql/mutations/award_emojis/base.rb
@@ -3,6 +3,10 @@
module Mutations
module AwardEmojis
class Base < BaseMutation
+ include ::Mutations::FindsByGid
+
+ NOT_EMOJI_AWARDABLE = 'You cannot award emoji to this resource.'
+
authorize :award_emoji
argument :awardable_id,
@@ -22,20 +26,15 @@ module Mutations
private
+ # TODO: remove this method when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
def find_object(id:)
- # TODO: remove this line when the compatibility layer is removed
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
- id = ::Types::GlobalIDType[::Awardable].coerce_isolated_input(id)
- GitlabSchema.find_by_gid(id)
+ super(id: ::Types::GlobalIDType[::Awardable].coerce_isolated_input(id))
end
- # Called by mutations methods after performing an authorization check
- # of an awardable object.
- def check_object_is_awardable!(object)
- unless object.is_a?(Awardable) && object.emoji_awardable?
- raise Gitlab::Graphql::Errors::ResourceNotAvailable,
- 'Cannot award emoji to this resource'
- end
+ def authorize!(object)
+ super
+ raise_resource_not_available_error!(NOT_EMOJI_AWARDABLE) unless object.emoji_awardable?
end
end
end
diff --git a/app/graphql/mutations/award_emojis/remove.rb b/app/graphql/mutations/award_emojis/remove.rb
index c654688c6dc..a9655daeea7 100644
--- a/app/graphql/mutations/award_emojis/remove.rb
+++ b/app/graphql/mutations/award_emojis/remove.rb
@@ -8,8 +8,6 @@ module Mutations
def resolve(args)
awardable = authorized_find!(id: args[:awardable_id])
- check_object_is_awardable!(awardable)
-
service = ::AwardEmojis::DestroyService.new(awardable, args[:name], current_user).execute
{
diff --git a/app/graphql/mutations/award_emojis/toggle.rb b/app/graphql/mutations/award_emojis/toggle.rb
index 679ec7a14ff..e741f972b1b 100644
--- a/app/graphql/mutations/award_emojis/toggle.rb
+++ b/app/graphql/mutations/award_emojis/toggle.rb
@@ -12,8 +12,6 @@ module Mutations
def resolve(args)
awardable = authorized_find!(id: args[:awardable_id])
- check_object_is_awardable!(awardable)
-
service = ::AwardEmojis::ToggleService.new(awardable, args[:name], current_user).execute
toggled_on = awardable.awarded_emoji?(args[:name], current_user)
diff --git a/app/graphql/mutations/concerns/mutations/finds_by_gid.rb b/app/graphql/mutations/concerns/mutations/finds_by_gid.rb
new file mode 100644
index 00000000000..157f87a413d
--- /dev/null
+++ b/app/graphql/mutations/concerns/mutations/finds_by_gid.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Mutations
+ module FindsByGid
+ def find_object(id:)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+end
diff --git a/app/graphql/mutations/issues/update.rb b/app/graphql/mutations/issues/update.rb
index 9b216b31f9b..d34e351b2a6 100644
--- a/app/graphql/mutations/issues/update.rb
+++ b/app/graphql/mutations/issues/update.rb
@@ -11,7 +11,7 @@ module Mutations
required: false,
description: copy_field_description(Types::IssueType, :title)
- argument :milestone_id, GraphQL::ID_TYPE,
+ argument :milestone_id, GraphQL::ID_TYPE, # rubocop: disable Graphql/IDType
required: false,
description: 'The ID of the milestone to assign to the issue. On update milestone will be removed if set to null'
diff --git a/app/graphql/mutations/releases/create.rb b/app/graphql/mutations/releases/create.rb
index 57c1541c368..156cd252848 100644
--- a/app/graphql/mutations/releases/create.rb
+++ b/app/graphql/mutations/releases/create.rb
@@ -40,12 +40,11 @@ module Mutations
authorize :create_release
- def resolve(project_path:, milestones: nil, assets: nil, **scalars)
+ def resolve(project_path:, assets: nil, **scalars)
project = authorized_find!(full_path: project_path)
params = {
**scalars,
- milestones: milestones.presence || [],
assets: assets.to_h
}.with_indifferent_access
diff --git a/app/graphql/mutations/releases/update.rb b/app/graphql/mutations/releases/update.rb
new file mode 100644
index 00000000000..bf72b907679
--- /dev/null
+++ b/app/graphql/mutations/releases/update.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Releases
+ class Update < Base
+ graphql_name 'ReleaseUpdate'
+
+ field :release,
+ Types::ReleaseType,
+ null: true,
+ description: 'The release after mutation.'
+
+ argument :tag_name, GraphQL::STRING_TYPE,
+ required: true, as: :tag,
+ description: 'Name of the tag associated with the release'
+
+ argument :name, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Name of the release'
+
+ argument :description, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Description (release notes) of the release'
+
+ argument :released_at, Types::TimeType,
+ required: false,
+ description: 'The release date'
+
+ argument :milestones, [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'The title of each milestone the release is associated with. GitLab Premium customers can specify group milestones.'
+
+ authorize :update_release
+
+ def ready?(**args)
+ if args.key?(:released_at) && args[:released_at].nil?
+ raise Gitlab::Graphql::Errors::ArgumentError,
+ 'if the releasedAt argument is provided, it cannot be null'
+ end
+
+ if args.key?(:milestones) && args[:milestones].nil?
+ raise Gitlab::Graphql::Errors::ArgumentError,
+ 'if the milestones argument is provided, it cannot be null'
+ end
+
+ super
+ end
+
+ def resolve(project_path:, **scalars)
+ project = authorized_find!(full_path: project_path)
+
+ params = scalars.with_indifferent_access
+
+ release_result = ::Releases::UpdateService.new(project, current_user, params).execute
+
+ if release_result[:status] == :success
+ {
+ release: release_result[:release],
+ errors: []
+ }
+ else
+ {
+ release: nil,
+ errors: [release_result[:message]]
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb
index 669b487db10..13b5672d750 100644
--- a/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb
+++ b/app/graphql/resolvers/error_tracking/sentry_error_stack_trace_resolver.rb
@@ -3,6 +3,8 @@
module Resolvers
module ErrorTracking
class SentryErrorStackTraceResolver < BaseResolver
+ type Types::ErrorTracking::SentryErrorStackTraceType, null: true
+
argument :id, ::Types::GlobalIDType[::Gitlab::ErrorTracking::DetailedError],
required: true,
description: 'ID of the Sentry issue'
diff --git a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb
index c5cf924ce7f..e844ffedbeb 100644
--- a/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb
+++ b/app/graphql/resolvers/error_tracking/sentry_errors_resolver.rb
@@ -4,19 +4,26 @@ module Resolvers
module ErrorTracking
class SentryErrorsResolver < BaseResolver
type Types::ErrorTracking::SentryErrorType.connection_type, null: true
+ extension Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension
+
+ argument :search_term, ::GraphQL::STRING_TYPE,
+ description: 'Search query for the Sentry error details',
+ required: false
+
+ # TODO: convert to Enum
+ argument :sort, ::GraphQL::STRING_TYPE,
+ description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default',
+ required: false
+
+ delegate :project, to: :object
def resolve(**args)
args[:cursor] = args.delete(:after)
- project = object.project
- result = ::ErrorTracking::ListIssuesService.new(
- project,
- context[:current_user],
- args
- ).execute
+ result = ::ErrorTracking::ListIssuesService.new(project, current_user, args).execute
- next_cursor = result[:pagination]&.dig('next', 'cursor')
- previous_cursor = result[:pagination]&.dig('previous', 'cursor')
+ next_cursor = result.dig(:pagination, 'next', 'cursor')
+ previous_cursor = result.dig(:pagination, 'previous', 'cursor')
issues = result[:issues]
# ReactiveCache is still fetching data
@@ -24,6 +31,10 @@ module Resolvers
Gitlab::Graphql::ExternallyPaginatedArray.new(previous_cursor, next_cursor, *issues)
end
+
+ def self.field_options
+ super.merge(connection: false) # we manage the pagination manually, so opt out of the connection field extension
+ end
end
end
end
diff --git a/app/graphql/types/error_tracking/sentry_error_collection_type.rb b/app/graphql/types/error_tracking/sentry_error_collection_type.rb
index 798e0433d06..49d5d62c860 100644
--- a/app/graphql/types/error_tracking/sentry_error_collection_type.rb
+++ b/app/graphql/types/error_tracking/sentry_error_collection_type.rb
@@ -9,27 +9,12 @@ module Types
authorize :read_sentry_issue
field :errors,
- Types::ErrorTracking::SentryErrorType.connection_type,
- connection: false,
- null: true,
description: "Collection of Sentry Errors",
- extensions: [Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension],
- resolver: Resolvers::ErrorTracking::SentryErrorsResolver do
- argument :search_term,
- String,
- description: 'Search query for the Sentry error details',
- required: false
- argument :sort,
- String,
- description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default',
- required: false
- end
- field :detailed_error, Types::ErrorTracking::SentryDetailedErrorType,
- null: true,
+ resolver: Resolvers::ErrorTracking::SentryErrorsResolver
+ field :detailed_error,
description: 'Detailed version of a Sentry error on the project',
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver
- field :error_stack_trace, Types::ErrorTracking::SentryErrorStackTraceType,
- null: true,
+ field :error_stack_trace,
description: 'Stack Trace of Sentry Error',
resolver: Resolvers::ErrorTracking::SentryErrorStackTraceResolver
field :external_url,
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 75ccac6d590..18576b4ca34 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -65,6 +65,7 @@ module Types
mount_mutation Mutations::Notes::RepositionImageDiffNote
mount_mutation Mutations::Notes::Destroy
mount_mutation Mutations::Releases::Create
+ mount_mutation Mutations::Releases::Update
mount_mutation Mutations::Terraform::State::Delete
mount_mutation Mutations::Terraform::State::Lock
mount_mutation Mutations::Terraform::State::Unlock
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 5a436886117..16f758c0c6b 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -238,8 +238,7 @@ module Types
field :jira_imports,
Types::JiraImportType.connection_type,
null: true,
- description: 'Jira imports into the project',
- resolver: Resolvers::Projects::JiraImportsResolver
+ description: 'Jira imports into the project'
field :services,
Types::Projects::ServiceType.connection_type,
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index 11c5369f726..ddba6589474 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -38,6 +38,8 @@ module Types
feature_flag: :user_group_counts
field :status, Types::UserStatusType, null: true,
description: 'User status'
+ field :location, ::GraphQL::STRING_TYPE, null: true,
+ description: 'The location of the user.'
field :project_memberships, Types::ProjectMemberType.connection_type, null: true,
description: 'Project memberships of the user',
method: :project_members
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 512649b3008..9a43a4a3a15 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -49,12 +49,12 @@ module ApplicationSettingsHelper
all_protocols_enabled? || Gitlab::CurrentSettings.enabled_git_access_protocol == 'http'
end
- def enabled_project_button(project, protocol)
+ def enabled_protocol_button(container, protocol)
case protocol
when 'ssh'
- ssh_clone_button(project, append_link: false)
+ ssh_clone_button(container, append_link: false)
else
- http_clone_button(project, append_link: false)
+ http_clone_button(container, append_link: false)
end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 981b5e4d92b..2faa24393cd 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -382,8 +382,7 @@ module BlobHelper
end
def show_suggest_pipeline_creation_celebration?
- Feature.enabled?(:suggest_pipeline, default_enabled: true) &&
- @blob.path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] &&
+ @blob.path == Gitlab::FileDetector::PATTERNS[:gitlab_ci] &&
@blob.auxiliary_viewer&.valid?(project: @project, sha: @commit.sha, user: current_user) &&
@project.uses_default_ci_config? &&
cookies[suggest_pipeline_commit_cookie_name].present?
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index c999d1f94ad..ea24f469ffa 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -58,10 +58,10 @@ module ButtonHelper
end
end
- def http_clone_button(project, append_link: true)
+ def http_clone_button(container, append_link: true)
protocol = gitlab_config.protocol.upcase
dropdown_description = http_dropdown_description(protocol)
- append_url = project.http_url_to_repo if append_link
+ append_url = container.http_url_to_repo if append_link
dropdown_item_with_description(protocol, dropdown_description, href: append_url, data: { clone_type: 'http' })
end
@@ -74,13 +74,13 @@ module ButtonHelper
end
end
- def ssh_clone_button(project, append_link: true)
+ def ssh_clone_button(container, append_link: true)
if Gitlab::CurrentSettings.user_show_add_ssh_key_message? &&
current_user.try(:require_ssh_key?)
- dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile")
+ dropdown_description = s_("MissingSSHKeyWarningLink|You won't be able to pull or push repositories via SSH until you add an SSH key to your profile")
end
- append_url = project.ssh_url_to_repo if append_link
+ append_url = container.ssh_url_to_repo if append_link
dropdown_item_with_description('SSH', dropdown_description, href: append_url, data: { clone_type: 'ssh' })
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index d6d06434590..69a2efebb1f 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -203,14 +203,6 @@ module DiffHelper
set_secure_cookie(:diff_view, params.delete(:view), type: CookiesHelper::COOKIE_TYPE_PERMANENT) if params[:view].present?
end
- def unified_diff_lines_view_type(project)
- if Feature.enabled?(:unified_diff_lines, project, default_enabled: true)
- 'inline'
- else
- diff_view
- end
- end
-
private
def diff_btn(title, name, selected)
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index e10e9a83b05..45f5281b515 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -51,7 +51,7 @@ module DropdownsHelper
default_label = data_attr[:default_label]
content_tag(:button, disabled: options[:disabled], class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
- output << icon('chevron-down')
+ output << sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
output.html_safe
end
end
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index c4487ae8e4a..491d2731e91 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -52,6 +52,8 @@ module EnvironmentHelper
s_('Deployment|failed')
when 'canceled'
s_('Deployment|canceled')
+ when 'skipped'
+ s_('Deployment|skipped')
end
klass = "ci-status ci-#{status.dasherize}"
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 29ead76a607..a0a840add94 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -189,6 +189,10 @@ module GroupsHelper
params.key?(:purchased_quantity) && params[:purchased_quantity].to_i > 0
end
+ def project_list_sort_by
+ @group_projects_sort || @sort || params[:sort] || sort_value_recently_created
+ end
+
private
def just_created?
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index de1e0e4e05e..2d47ee89d11 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -31,7 +31,7 @@ module SearchHelper
[
resources_results,
generic_results
- ].flatten.uniq do |item|
+ ].flatten do |item|
item[:label]
end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 10174e5d719..2166c3faec4 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -40,6 +40,7 @@ module SortingHelper
sort_value_latest_activity => sort_title_latest_activity,
sort_value_recently_created => sort_title_created_date,
sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name_desc,
sort_value_stars_desc => sort_title_stars
}
@@ -95,8 +96,8 @@ module SortingHelper
sort_value_name_desc => sort_title_name_desc,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
- sort_value_recently_updated => sort_title_recently_updated,
- sort_value_oldest_updated => sort_title_oldest_updated
+ sort_value_latest_activity => sort_title_recently_updated,
+ sort_value_oldest_activity => sort_title_oldest_updated
}
end
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index 13bf9c92d52..d6a4d6ac57a 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -15,9 +15,11 @@ module StorageHelper
counter_wikis: storage_counter(statistics.wiki_size),
counter_build_artifacts: storage_counter(statistics.build_artifacts_size),
counter_lfs_objects: storage_counter(statistics.lfs_objects_size),
- counter_snippets: storage_counter(statistics.snippets_size)
+ counter_snippets: storage_counter(statistics.snippets_size),
+ counter_packages: storage_counter(statistics.packages_size),
+ counter_uploads: storage_counter(statistics.uploads_size)
}
- _("Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / LFS: %{counter_lfs_objects} / Snippets: %{counter_snippets}") % counters
+ _("Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / LFS: %{counter_lfs_objects} / Snippets: %{counter_snippets} / Packages: %{counter_packages} / Uploads: %{counter_uploads}") % counters
end
end
diff --git a/app/helpers/suggest_pipeline_helper.rb b/app/helpers/suggest_pipeline_helper.rb
index 3151b792344..f0a12f0e268 100644
--- a/app/helpers/suggest_pipeline_helper.rb
+++ b/app/helpers/suggest_pipeline_helper.rb
@@ -2,8 +2,6 @@
module SuggestPipelineHelper
def should_suggest_gitlab_ci_yml?
- Feature.enabled?(:suggest_pipeline, default_enabled: true) &&
- current_user &&
- params[:suggest_gitlab_ci_yml] == 'true'
+ current_user && params[:suggest_gitlab_ci_yml] == 'true'
end
end
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index e93c1b82cd7..a06a31ddf32 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -57,7 +57,10 @@ module UserCalloutsHelper
end
def show_registration_enabled_user_callout?
- current_user&.admin? && signup_enabled? && !user_dismissed?(REGISTRATION_ENABLED_CALLOUT)
+ !Gitlab.com? &&
+ current_user&.admin? &&
+ signup_enabled? &&
+ !user_dismissed?(REGISTRATION_ENABLED_CALLOUT)
end
private
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 7d4ab192f2f..fbd95094fbd 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -123,6 +123,19 @@ module UsersHelper
}
end
+ def unblock_user_modal_data(user)
+ {
+ path: unblock_admin_user_path(user),
+ method: 'put',
+ modal_attributes: {
+ title: s_('AdminUsers|Unblock user %{username}?') % { username: sanitize_name(user.name) },
+ message: s_('AdminUsers|You can always block their account again if needed.'),
+ okVariant: 'info',
+ okTitle: s_('AdminUsers|Unblock')
+ }.to_json
+ }
+ end
+
def user_block_effects
header = tag.p s_('AdminUsers|Blocking user has the following effects:')
diff --git a/app/models/analytics/devops_adoption/segment.rb b/app/models/analytics/devops_adoption/segment.rb
index 71d4a312627..baab5b94126 100644
--- a/app/models/analytics/devops_adoption/segment.rb
+++ b/app/models/analytics/devops_adoption/segment.rb
@@ -7,7 +7,7 @@ class Analytics::DevopsAdoption::Segment < ApplicationRecord
has_many :groups, through: :segment_selections
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
- validate :validate_segment_count
+ validate :validate_segment_count, on: :create
accepts_nested_attributes_for :segment_selections, allow_destroy: true
diff --git a/app/models/analytics/devops_adoption/segment_selection.rb b/app/models/analytics/devops_adoption/segment_selection.rb
index 6b70c13a773..8f95ce088a2 100644
--- a/app/models/analytics/devops_adoption/segment_selection.rb
+++ b/app/models/analytics/devops_adoption/segment_selection.rb
@@ -14,7 +14,7 @@ class Analytics::DevopsAdoption::SegmentSelection < ApplicationRecord
validates :group_id, uniqueness: { scope: :segment_id, if: :group }
validate :exclusive_project_or_group
- validate :validate_selection_count
+ validate :validate_selection_count, on: :create
private
@@ -27,9 +27,9 @@ class Analytics::DevopsAdoption::SegmentSelection < ApplicationRecord
def validate_selection_count
return unless segment
- selection_count_for_segment = self.class.where(segment: segment).count
-
- if selection_count_for_segment >= ALLOWED_SELECTIONS_PER_SEGMENT
+ # handle single model creation and bulk creation from accepts_nested_attributes_for
+ selections = segment.segment_selections + [self]
+ if selections.reject(&:marked_for_destruction?).uniq.size > ALLOWED_SELECTIONS_PER_SEGMENT
errors.add(:segment, s_('DevopsAdoptionSegmentSelection|The maximum number of selections has been reached'))
end
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index 34030e079c7..f0f4d3ef339 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -59,6 +59,25 @@ class BulkImports::Entity < ApplicationRecord
end
end
+ def update_tracker_for(relation:, has_next_page:, next_page: nil)
+ attributes = {
+ relation: relation,
+ has_next_page: has_next_page,
+ next_page: next_page,
+ bulk_import_entity_id: id
+ }
+
+ trackers.upsert(attributes, unique_by: %i[bulk_import_entity_id relation])
+ end
+
+ def has_next_page?(relation)
+ trackers.find_by(relation: relation)&.has_next_page
+ end
+
+ def next_page_for(relation)
+ trackers.find_by(relation: relation)&.next_page
+ end
+
private
def validate_parent_is_a_group
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 5b23cf46fdb..445775fc6f3 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -206,7 +206,7 @@ module Ci
override :dependency_variables
def dependency_variables
- return [] unless ::Feature.enabled?(:ci_bridge_dependency_variables, project)
+ return [] unless ::Feature.enabled?(:ci_bridge_dependency_variables, project, default_enabled: true)
super
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 84abd01786d..4b1299c7aee 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -379,8 +379,16 @@ module Ci
Ci::BuildRunnerSession.where(build: build).delete_all
end
- after_transition any => [:skipped, :canceled] do |build|
- build.deployment&.cancel
+ after_transition any => [:skipped, :canceled] do |build, transition|
+ if Feature.enabled?(:cd_skipped_deployment_status, build.project)
+ if transition.to_name == :skipped
+ build.deployment&.skip
+ else
+ build.deployment&.cancel
+ end
+ else
+ build.deployment&.cancel
+ end
end
end
@@ -915,6 +923,14 @@ module Ci
coverage_report
end
+ def collect_codequality_reports!(codequality_report)
+ each_report(Ci::JobArtifact::CODEQUALITY_REPORT_FILE_TYPES) do |file_type, blob|
+ Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, codequality_report)
+ end
+
+ codequality_report
+ end
+
def collect_terraform_reports!(terraform_reports)
each_report(::Ci::JobArtifact::TERRAFORM_REPORT_FILE_TYPES) do |file_type, blob, report_artifact|
::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, terraform_reports, artifact: report_artifact)
diff --git a/app/models/ci/build_trace_chunks/fog.rb b/app/models/ci/build_trace_chunks/fog.rb
index d3051e3dadc..31afefba504 100644
--- a/app/models/ci/build_trace_chunks/fog.rb
+++ b/app/models/ci/build_trace_chunks/fog.rb
@@ -14,11 +14,15 @@ module Ci
end
def set_data(model, new_data)
- # TODO: Support AWS S3 server side encryption
- files.create({
- key: key(model),
- body: new_data
- })
+ if Feature.enabled?(:ci_live_trace_use_fog_attributes)
+ files.create(create_attributes(model, new_data))
+ else
+ # TODO: Support AWS S3 server side encryption
+ files.create({
+ key: key(model),
+ body: new_data
+ })
+ end
end
def append_data(model, new_data, offset)
@@ -57,6 +61,13 @@ module Ci
key_raw(model.build_id, model.chunk_index)
end
+ def create_attributes(model, new_data)
+ {
+ key: key(model),
+ body: new_data
+ }.merge(object_store_config.fog_attributes)
+ end
+
def key_raw(build_id, chunk_index)
"tmp/builds/#{build_id.to_i}/chunks/#{chunk_index.to_i}.log"
end
@@ -84,6 +95,14 @@ module Ci
def object_store
Gitlab.config.artifacts.object_store
end
+
+ def object_store_raw_config
+ object_store
+ end
+
+ def object_store_config
+ @object_store_config ||= ::ObjectStorage::Config.new(object_store_raw_config)
+ end
end
end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 7cedd13b407..c80d50ea131 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -7,15 +7,13 @@ module Ci
include UpdateProjectStatistics
include UsageStatistics
include Sortable
- include IgnorableColumns
include Artifactable
include FileStoreMounter
extend Gitlab::Ci::Model
- ignore_columns :locked, remove_after: '2020-07-22', remove_with: '13.4'
-
TEST_REPORT_FILE_TYPES = %w[junit].freeze
COVERAGE_REPORT_FILE_TYPES = %w[cobertura].freeze
+ CODEQUALITY_REPORT_FILE_TYPES = %w[codequality].freeze
ACCESSIBILITY_REPORT_FILE_TYPES = %w[accessibility].freeze
NON_ERASABLE_FILE_TYPES = %w[trace].freeze
TERRAFORM_REPORT_FILE_TYPES = %w[terraform].freeze
@@ -157,6 +155,10 @@ module Ci
with_file_types(COVERAGE_REPORT_FILE_TYPES)
end
+ scope :codequality_reports, -> do
+ with_file_types(CODEQUALITY_REPORT_FILE_TYPES)
+ end
+
scope :terraform_reports, -> do
with_file_types(TERRAFORM_REPORT_FILE_TYPES)
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 8707d635e03..4bfb38cbe2d 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -956,6 +956,14 @@ module Ci
end
end
+ def codequality_reports
+ Gitlab::Ci::Reports::CodequalityReports.new.tap do |codequality_reports|
+ latest_report_builds(Ci::JobArtifact.codequality_reports).each do |build|
+ build.collect_codequality_reports!(codequality_reports)
+ end
+ end
+ end
+
def terraform_reports
::Gitlab::Ci::Reports::TerraformReports.new.tap do |terraform_reports|
latest_report_builds(::Ci::JobArtifact.terraform_reports).each do |build|
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index d1d6defb713..6f4b273a2c8 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -4,8 +4,8 @@ require 'openssl'
module Clusters
module Applications
- # DEPRECATED: This model represents the Helm 2 Tiller server, and is no longer being actively used.
- # It is being kept around for a potential cleanup of the unused Tiller server.
+ # DEPRECATED: This model represents the Helm 2 Tiller server.
+ # It is being kept around to enable the cleanup of the unused Tiller server.
class Helm < ApplicationRecord
self.table_name = 'clusters_applications_helm'
@@ -27,29 +27,11 @@ module Clusters
end
def set_initial_status
- return unless not_installable?
-
- self.status = status_states[:installable] if cluster&.platform_kubernetes_active?
- end
-
- # It can only be uninstalled if there are no other applications installed
- # or with intermitent installation statuses in the database.
- def allowed_to_uninstall?
- strong_memoize(:allowed_to_uninstall) do
- applications = nil
-
- Clusters::Cluster::APPLICATIONS.each do |application_name, klass|
- next if application_name == 'helm'
-
- extra_apps = Clusters::Applications::Helm.where('EXISTS (?)', klass.select(1).where(cluster_id: cluster_id))
-
- applications = applications ? applications.or(extra_apps) : extra_apps
- end
-
- !applications.exists?
- end
+ # The legacy Tiller server is not installable, which is the initial status of every app
end
+ # DEPRECATED: This command is only for development and testing purposes, to simulate
+ # a Helm 2 cluster with an existing Tiller server.
def install_command
Gitlab::Kubernetes::Helm::V2::InitCommand.new(
name: name,
@@ -70,13 +52,6 @@ module Clusters
ca_key.present? && ca_cert.present?
end
- def post_uninstall
- cluster.kubeclient.delete_namespace(Gitlab::Kubernetes::Helm::NAMESPACE)
- rescue Kubeclient::ResourceNotFoundError
- # we actually don't care if the namespace is not present
- # since we want to delete it anyway.
- end
-
private
def files
diff --git a/app/models/concerns/enums/internal_id.rb b/app/models/concerns/enums/internal_id.rb
index f01bd60ef16..b08c05b1934 100644
--- a/app/models/concerns/enums/internal_id.rb
+++ b/app/models/concerns/enums/internal_id.rb
@@ -15,7 +15,8 @@ module Enums
operations_user_lists: 7,
alert_management_alerts: 8,
sprints: 9, # iterations
- design_management_designs: 10
+ design_management_designs: 10,
+ incident_management_oncall_schedules: 11
}
end
end
diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb
index 7be4a26d4fa..82055822cfb 100644
--- a/app/models/concerns/optimized_issuable_label_filter.rb
+++ b/app/models/concerns/optimized_issuable_label_filter.rb
@@ -1,6 +1,15 @@
# frozen_string_literal: true
module OptimizedIssuableLabelFilter
+ extend ActiveSupport::Concern
+
+ prepended do
+ extend Gitlab::Cache::RequestCache
+
+ # Avoid repeating label queries times when the finder is instantiated multiple times during the request.
+ request_cache(:find_label_ids) { [root_namespace.id, params.label_names] }
+ end
+
def by_label(items)
return items unless params.labels?
@@ -41,7 +50,7 @@ module OptimizedIssuableLabelFilter
def issuables_with_selected_labels(items, target_model)
if root_namespace
- all_label_ids = find_label_ids(root_namespace)
+ all_label_ids = find_label_ids
# Found less labels in the DB than we were searching for. Return nothing.
return items.none if all_label_ids.size != params.label_names.size
@@ -57,18 +66,20 @@ module OptimizedIssuableLabelFilter
items
end
- def find_label_ids(root_namespace)
- finder_params = {
- include_subgroups: true,
- include_ancestor_groups: true,
- include_descendant_groups: true,
- group: root_namespace,
- title: params.label_names
- }
-
- LabelsFinder
- .new(nil, finder_params)
- .execute(skip_authorization: true)
+ def find_label_ids
+ group_labels = Label
+ .where(project_id: nil)
+ .where(title: params.label_names)
+ .where(group_id: root_namespace.self_and_descendants.select(:id))
+
+ project_labels = Label
+ .where(group_id: nil)
+ .where(title: params.label_names)
+ .where(project_id: Project.select(:id).where(namespace_id: root_namespace.self_and_descendants.select(:id)))
+
+ Label
+ .from_union([group_labels, project_labels], remove_duplicates: false)
+ .reorder(nil)
.pluck(:title, :id)
.group_by(&:first)
.values
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 36ac1bdb236..ad741366a74 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -41,8 +41,8 @@ class Deployment < ApplicationRecord
scope :visible, -> { where(status: %i[running success failed canceled]) }
scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success }
scope :active, -> { where(status: %i[created running]) }
- scope :older_than, -> (deployment) { where('id < ?', deployment.id) }
- scope :with_deployable, -> { includes(:deployable).where('deployable_id IS NOT NULL') }
+ scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) }
+ scope :with_deployable, -> { joins('INNER JOIN ci_builds ON ci_builds.id = deployments.deployable_id').preload(:deployable) }
FINISHED_STATUSES = %i[success failed canceled].freeze
@@ -63,6 +63,10 @@ class Deployment < ApplicationRecord
transition any - [:canceled] => :canceled
end
+ event :skip do
+ transition any - [:skipped] => :skipped
+ end
+
before_transition any => FINISHED_STATUSES do |deployment|
deployment.finished_at = Time.current
end
@@ -105,7 +109,8 @@ class Deployment < ApplicationRecord
running: 1,
success: 2,
failed: 3,
- canceled: 4
+ canceled: 4,
+ skipped: 5
}
def self.last_for_environment(environment)
@@ -144,6 +149,10 @@ class Deployment < ApplicationRecord
project.repository.delete_refs(*ref_paths.flatten)
end
end
+
+ def latest_for_sha(sha)
+ where(sha: sha).order(id: :desc).take
+ end
end
def commit
@@ -297,6 +306,8 @@ class Deployment < ApplicationRecord
drop
when 'canceled'
cancel
+ when 'skipped'
+ skip
else
raise ArgumentError, "The status #{status.inspect} is invalid"
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 4b2e62bf761..944a64f5419 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -19,7 +19,7 @@ class DiffNote < Note
# EE might have added a type when the module was prepended
validates :noteable_type, inclusion: { in: -> (_note) { noteable_types } }
validate :positions_complete
- validate :verify_supported
+ validate :verify_supported, unless: :importing?
before_validation :set_line_code, if: :on_text?, unless: :importing?
after_save :keep_around_commits, unless: :importing?
@@ -149,7 +149,7 @@ class DiffNote < Note
end
def supported?
- for_commit? || for_design? || self.noteable.has_complete_diff_refs?
+ for_commit? || for_design? || self.noteable&.has_complete_diff_refs?
end
def set_line_code
diff --git a/app/models/environment.rb b/app/models/environment.rb
index deded3eeae0..92e1caf5227 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -60,6 +60,7 @@ class Environment < ApplicationRecord
addressable_url: true
delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
+ delegate :auto_rollback_enabled?, to: :project
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
@@ -240,10 +241,6 @@ class Environment < ApplicationRecord
def cancel_deployment_jobs!
jobs = active_deployments.with_deployable
jobs.each do |deployment|
- # guard against data integrity issues,
- # for example https://gitlab.com/gitlab-org/gitlab/-/issues/218659#note_348823660
- next unless deployment.deployable
-
Gitlab::OptimisticLocking.retry_lock(deployment.deployable) do |deployable|
deployable.cancel! if deployable&.cancelable?
end
diff --git a/app/models/exported_protected_branch.rb b/app/models/exported_protected_branch.rb
new file mode 100644
index 00000000000..6e8abbc2389
--- /dev/null
+++ b/app/models/exported_protected_branch.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ExportedProtectedBranch < ProtectedBranch
+ has_many :push_access_levels, -> { where(deploy_key_id: nil) }, class_name: "ProtectedBranch::PushAccessLevel", foreign_key: :protected_branch_id
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7dc18cacd7c..14eed4bd607 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -328,7 +328,9 @@ class Issue < ApplicationRecord
related_issues = ::Issue
.select(['issues.*', 'issue_links.id AS issue_link_id',
'issue_links.link_type as issue_link_type_value',
- 'issue_links.target_id as issue_link_source_id'])
+ 'issue_links.target_id as issue_link_source_id',
+ 'issue_links.created_at as issue_link_created_at',
+ 'issue_links.updated_at as issue_link_updated_at'])
.joins("INNER JOIN issue_links ON
(issue_links.source_id = issues.id AND issue_links.target_id = #{id})
OR
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d379f85bc15..46673917008 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -314,6 +314,31 @@ class MergeRequest < ApplicationRecord
scope :with_jira_issue_keys, -> { where('title ~ :regex OR merge_requests.description ~ :regex', regex: Gitlab::Regex.jira_issue_key_regex.source) }
+ scope :review_requested, -> do
+ where(reviewers_subquery.exists)
+ end
+
+ scope :no_review_requested, -> do
+ where(reviewers_subquery.exists.not)
+ end
+
+ scope :review_requested_to, ->(user) do
+ where(
+ reviewers_subquery
+ .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user))
+ .exists
+ )
+ end
+
+ scope :no_review_requested_to, ->(user) do
+ where(
+ reviewers_subquery
+ .where(Arel::Table.new("#{to_ability_name}_reviewers")[:user_id].eq(user))
+ .exists
+ .not
+ )
+ end
+
after_save :keep_around_commit, unless: :importing?
alias_attribute :project, :target_project
@@ -361,6 +386,12 @@ class MergeRequest < ApplicationRecord
end
end
+ def self.reviewers_subquery
+ MergeRequestReviewer.arel_table
+ .project('true')
+ .where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id"))
+ end
+
def rebase_in_progress?
rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)
end
diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb
index 1cb49c0cd76..c4e5274f832 100644
--- a/app/models/merge_request_reviewer.rb
+++ b/app/models/merge_request_reviewer.rb
@@ -2,5 +2,5 @@
class MergeRequestReviewer < ApplicationRecord
belongs_to :merge_request
- belongs_to :reviewer, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees
+ belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 232d0a6b05d..238e8f70778 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -28,6 +28,7 @@ class Namespace < ApplicationRecord
has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
+ has_many :namespace_onboarding_actions
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
diff --git a/app/models/namespace_onboarding_action.rb b/app/models/namespace_onboarding_action.rb
new file mode 100644
index 00000000000..ea0d9d495ae
--- /dev/null
+++ b/app/models/namespace_onboarding_action.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class NamespaceOnboardingAction < ApplicationRecord
+ belongs_to :namespace
+end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index 9855731778f..e33d09559ae 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -2,6 +2,8 @@
module Pages
class LookupPath
+ include Gitlab::Utils::StrongMemoize
+
def initialize(project, trim_prefix: nil, domain: nil)
@project = project
@domain = domain
@@ -37,37 +39,28 @@ module Pages
attr_reader :project, :trim_prefix, :domain
- def artifacts_archive
- return unless Feature.enabled?(:pages_serve_from_artifacts_archive, project)
-
- project.pages_metadatum.artifacts_archive
- end
-
def deployment
- return unless Feature.enabled?(:pages_serve_from_deployments, project)
+ strong_memoize(:deployment) do
+ next unless Feature.enabled?(:pages_serve_from_deployments, project)
- project.pages_metadatum.pages_deployment
+ project.pages_metadatum.pages_deployment
+ end
end
def zip_source
- source = deployment || artifacts_archive
-
- return unless source&.file
-
- return if source.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project)
+ return unless deployment&.file
- # artifacts archive doesn't support this
- file_count = source.file_count if source.respond_to?(:file_count)
+ return if deployment.file.file_storage? && !Feature.enabled?(:pages_serve_with_zip_file_protocol, project)
- global_id = ::Gitlab::GlobalId.build(source, id: source.id).to_s
+ global_id = ::Gitlab::GlobalId.build(deployment, id: deployment.id).to_s
{
type: 'zip',
- path: source.file.url_or_file_path(expire_at: 1.day.from_now),
+ path: deployment.file.url_or_file_path(expire_at: 1.day.from_now),
global_id: global_id,
- sha256: source.file_sha256,
- file_size: source.size,
- file_count: file_count
+ sha256: deployment.file_sha256,
+ file_size: deployment.size,
+ file_count: deployment.file_count
}
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 8192310ddfb..4004ea9a662 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -34,10 +34,10 @@ class PagesDomain < ApplicationRecord
validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? }
- default_value_for(:auto_ssl_enabled, allow_nil: false) { ::Gitlab::LetsEncrypt.enabled? }
- default_value_for :scope, allow_nil: false, value: :project
- default_value_for :wildcard, allow_nil: false, value: false
- default_value_for :usage, allow_nil: false, value: :pages
+ default_value_for(:auto_ssl_enabled, allows_nil: false) { ::Gitlab::LetsEncrypt.enabled? }
+ default_value_for :scope, allows_nil: false, value: :project
+ default_value_for :wildcard, allows_nil: false, value: false
+ default_value_for :usage, allows_nil: false, value: :pages
attr_encrypted :key,
mode: :per_attribute_iv_and_salt,
diff --git a/app/models/project.rb b/app/models/project.rb
index ebd8e56246d..acdacd357c2 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -222,6 +222,7 @@ class Project < ApplicationRecord
has_many :snippets, class_name: 'ProjectSnippet'
has_many :hooks, class_name: 'ProjectHook'
has_many :protected_branches
+ has_many :exported_protected_branches
has_many :protected_tags
has_many :repository_languages, -> { order "share DESC" }
has_many :designs, inverse_of: :project, class_name: 'DesignManagement::Design'
diff --git a/app/models/release.rb b/app/models/release.rb
index c56df0a6aa3..bebf91fb247 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -29,6 +29,8 @@ class Release < ApplicationRecord
scope :preloaded, -> { includes(:evidences, :milestones, project: [:project_feature, :route, { namespace: :route }]) }
scope :with_project_and_namespace, -> { includes(project: :namespace) }
scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
+ scope :without_evidence, -> { left_joins(:evidences).where(::Releases::Evidence.arel_table[:id].eq(nil)) }
+ scope :released_within_2hrs, -> { where(released_at: Time.zone.now - 1.hour..Time.zone.now + 1.hour) }
# Sorting
scope :order_created, -> { reorder('created_at ASC') }
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index dc370b46bda..2e1a2e8e2b2 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -213,7 +213,8 @@ class Snippet < ApplicationRecord
def blobs
return [] unless repository_exists?
- repository.ls_files(default_branch).map { |file| Blob.lazy(repository, default_branch, file) }
+ branch = default_branch
+ list_files(branch).map { |file| Blob.lazy(repository, branch, file) }
end
def hook_attrs
diff --git a/app/models/snippet_blob.rb b/app/models/snippet_blob.rb
index cf1ab089829..bad24cc45f6 100644
--- a/app/models/snippet_blob.rb
+++ b/app/models/snippet_blob.rb
@@ -21,6 +21,10 @@ class SnippetBlob
data.bytesize
end
+ def commit_id
+ nil
+ end
+
def data
snippet.content
end
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 0ddf2c5fbcd..8dd471b259e 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class SystemNoteMetadata < ApplicationRecord
+ include Importable
+
# These notes's action text might contain a reference that is external.
# We should always force a deep validation upon references that are found
# in this note type.
@@ -23,7 +25,7 @@ class SystemNoteMetadata < ApplicationRecord
status alert_issue_added relate unrelate new_alert_added severity
].freeze
- validates :note, presence: true
+ validates :note, presence: true, unless: :importing?
validates :action, inclusion: { in: :icon_types }, allow_nil: true
belongs_to :note
diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb
index d329b429c9d..19700587f09 100644
--- a/app/models/terraform/state.rb
+++ b/app/models/terraform/state.rb
@@ -3,7 +3,6 @@
module Terraform
class State < ApplicationRecord
include UsageStatistics
- include FileStoreMounter
include IgnorableColumns
# These columns are being removed since geo replication falls to the versioned state
# Tracking in https://gitlab.com/gitlab-org/gitlab/-/issues/258262
@@ -35,20 +34,9 @@ module Terraform
format: { with: HEX_REGEXP, message: 'only allows hex characters' }
default_value_for(:uuid, allows_nil: false) { SecureRandom.hex(UUID_LENGTH / 2) }
- default_value_for(:versioning_enabled, true)
-
- mount_file_store_uploader StateUploader
-
- def file_store
- super || StateUploader.default_store
- end
def latest_file
- if versioning_enabled?
- latest_version&.file
- else
- latest_version&.file || file
- end
+ latest_version&.file
end
def locked?
@@ -56,13 +44,14 @@ module Terraform
end
def update_file!(data, version:, build:)
+ # This check is required to maintain backwards compatibility with
+ # states that were created prior to versioning being supported.
+ # This can be removed in 14.0 when support for these states is dropped.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/258960
if versioning_enabled?
create_new_version!(data: data, version: version, build: build)
- elsif latest_version.present?
- migrate_legacy_version!(data: data, version: version, build: build)
else
- self.file = data
- save!
+ migrate_legacy_version!(data: data, version: version, build: build)
end
end
diff --git a/app/models/terraform/state_version.rb b/app/models/terraform/state_version.rb
index cc5d94b8e09..19d708616fc 100644
--- a/app/models/terraform/state_version.rb
+++ b/app/models/terraform/state_version.rb
@@ -10,9 +10,9 @@ module Terraform
scope :ordered_by_version_desc, -> { order(version: :desc) }
- default_value_for(:file_store) { VersionedStateUploader.default_store }
+ default_value_for(:file_store) { StateUploader.default_store }
- mount_file_store_uploader VersionedStateUploader
+ mount_file_store_uploader StateUploader
delegate :project_id, :uuid, to: :terraform_state, allow_nil: true
diff --git a/app/models/user.rb b/app/models/user.rb
index be64e057d59..032ad26455c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -166,6 +166,7 @@ class User < ApplicationRecord
has_many :issue_assignees, inverse_of: :assignee
has_many :merge_request_assignees, inverse_of: :assignee
+ has_many :merge_request_reviewers, inverse_of: :reviewer
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, class_name: "MergeRequest", through: :merge_request_assignees, source: :merge_request
@@ -587,11 +588,13 @@ class User < ApplicationRecord
sanitized_order_sql = Arel.sql(sanitize_sql_array([order, query: query]))
- where(
- fuzzy_arel_match(:name, query, lower_exact_match: true)
- .or(fuzzy_arel_match(:username, query, lower_exact_match: true))
- .or(arel_table[:email].eq(query))
- ).reorder(sanitized_order_sql, :name)
+ search_query = if Feature.enabled?(:user_search_secondary_email)
+ search_with_secondary_emails(query)
+ else
+ search_without_secondary_emails(query)
+ end
+
+ search_query.reorder(sanitized_order_sql, :name)
end
# Limits the result set to users _not_ in the given query/list of IDs.
@@ -606,6 +609,18 @@ class User < ApplicationRecord
reorder(:name)
end
+ def search_without_secondary_emails(query)
+ return none if query.blank?
+
+ query = query.downcase
+
+ where(
+ fuzzy_arel_match(:name, query, lower_exact_match: true)
+ .or(fuzzy_arel_match(:username, query, lower_exact_match: true))
+ .or(arel_table[:email].eq(query))
+ )
+ end
+
# searches user by given pattern
# it compares name, email, username fields and user's secondary emails with given pattern
# This method uses ILIKE on PostgreSQL.
@@ -616,15 +631,16 @@ class User < ApplicationRecord
query = query.downcase
email_table = Email.arel_table
- matched_by_emails_user_ids = email_table
+ matched_by_email_user_id = email_table
.project(email_table[:user_id])
.where(email_table[:email].eq(query))
+ .take(1) # at most 1 record as there is a unique constraint
where(
fuzzy_arel_match(:name, query)
.or(fuzzy_arel_match(:username, query))
.or(arel_table[:email].eq(query))
- .or(arel_table[:id].in(matched_by_emails_user_ids))
+ .or(arel_table[:id].eq(matched_by_email_user_id))
)
end
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index cfad58fc0db..ad5651f9439 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -26,7 +26,8 @@ class UserCallout < ApplicationRecord
suggest_pipeline: 22,
customize_homepage: 23,
feature_flags_new_version: 24,
- registration_enabled_callout: 25
+ registration_enabled_callout: 25,
+ new_user_signups_cap_reached: 26 # EE-only
}
validates :user, presence: true
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index 5cfbcfec5c0..f49a6ee8498 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -27,3 +27,5 @@ class IssuablePolicy < BasePolicy
prevent :award_emoji
end
end
+
+IssuablePolicy.prepend_if_ee('EE::IssuablePolicy')
diff --git a/app/presenters/projects/import_export/project_export_presenter.rb b/app/presenters/projects/import_export/project_export_presenter.rb
index 8f3fc53af10..b52f3411c49 100644
--- a/app/presenters/projects/import_export/project_export_presenter.rb
+++ b/app/presenters/projects/import_export/project_export_presenter.rb
@@ -15,6 +15,10 @@ module Projects
self.respond_to?(:override_description) ? override_description : super
end
+ def protected_branches
+ project.exported_protected_branches
+ end
+
private
def converted_group_members
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index e46b269ea35..afd4d5b9a2b 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -2,6 +2,9 @@
class MergeRequestWidgetEntity < Grape::Entity
include RequestAwareEntity
+ include ProjectsHelper
+ include ApplicationHelper
+ include ApplicationSettingsHelper
SUGGEST_PIPELINE = 'suggest_pipeline'
@@ -48,6 +51,10 @@ class MergeRequestWidgetEntity < Grape::Entity
help_page_path('user/project/merge_requests/resolve_conflicts.md')
end
+ expose :reviewing_and_managing_merge_requests_docs_path do |merge_request|
+ help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: "checkout-merge-requests-locally-through-the-head-ref")
+ end
+
expose :merge_request_pipelines_docs_path do |merge_request|
help_page_path('ci/merge_request_pipelines/index.md')
end
@@ -67,15 +74,15 @@ class MergeRequestWidgetEntity < Grape::Entity
)
end
- expose :user_callouts_path, if: -> (*) { Feature.enabled?(:suggest_pipeline, default_enabled: true) } do |_merge_request|
+ expose :user_callouts_path do |_merge_request|
user_callouts_path
end
- expose :suggest_pipeline_feature_id, if: -> (*) { Feature.enabled?(:suggest_pipeline, default_enabled: true) } do |_merge_request|
+ expose :suggest_pipeline_feature_id do |_merge_request|
SUGGEST_PIPELINE
end
- expose :is_dismissed_suggest_pipeline, if: -> (*) { Feature.enabled?(:suggest_pipeline, default_enabled: true) } do |_merge_request|
+ expose :is_dismissed_suggest_pipeline do |_merge_request|
current_user && current_user.dismissed_callout?(feature_name: SUGGEST_PIPELINE)
end
@@ -87,6 +94,10 @@ class MergeRequestWidgetEntity < Grape::Entity
new_project_pipeline_path(merge_request.project)
end
+ expose :source_project_default_url do |merge_request|
+ merge_request.source_project && default_url_to_repo(merge_request.source_project)
+ end
+
# Rendering and redacting Markdown can be expensive. These links are
# just nice to have in the merge request widget, so only
# include them if they are explicitly requested on first load.
diff --git a/app/services/issuable/import_csv/base_service.rb b/app/services/issuable/import_csv/base_service.rb
index bf5f643a51b..77a096e7586 100644
--- a/app/services/issuable/import_csv/base_service.rb
+++ b/app/services/issuable/import_csv/base_service.rb
@@ -38,7 +38,7 @@ module Issuable
def with_csv_lines
csv_data = @csv_io.open(&:read).force_encoding(Encoding::UTF_8)
- verify_headers!(csv_data)
+ validate_headers_presence!(csv_data.lines.first)
csv_parsing_params = {
col_sep: detect_col_sep(csv_data.lines.first),
@@ -49,9 +49,9 @@ module Issuable
CSV.new(csv_data, csv_parsing_params).each.with_index(2)
end
- def verify_headers!(data)
- headers = data.lines.first.downcase
- return if headers.include?('title') && headers.include?('description')
+ def validate_headers_presence!(headers)
+ headers.downcase! if headers
+ return if headers && headers.include?('title') && headers.include?('description')
raise CSV::MalformedCSVError
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index b9c579a130f..f4a08169af7 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -125,8 +125,6 @@ module Projects
end
def create_pages_deployment(artifacts_path, build)
- return unless Feature.enabled?(:zip_pages_deployments, project, default_enabled: true)
-
# we're using the full archive and pages daemon needs to read it
# so we want the total count from entries, not only "public/" directory
# because it better approximates work we need to do before we can serve the site
diff --git a/app/services/releases/base_service.rb b/app/services/releases/base_service.rb
index 38ef80ced56..d0e1577bd8d 100644
--- a/app/services/releases/base_service.rb
+++ b/app/services/releases/base_service.rb
@@ -11,8 +11,6 @@ module Releases
@project, @current_user, @params = project, user, params.dup
end
- delegate :repository, to: :project
-
def tag_name
params[:tag]
end
@@ -39,22 +37,18 @@ module Releases
end
end
- def existing_tag
- strong_memoize(:existing_tag) do
- repository.find_tag(tag_name)
- end
- end
-
- def tag_exist?
- existing_tag.present?
- end
-
def repository
strong_memoize(:repository) do
project.repository
end
end
+ def existing_tag
+ strong_memoize(:existing_tag) do
+ repository.find_tag(tag_name)
+ end
+ end
+
def milestones
return [] unless param_for_milestone_titles_provided?
@@ -78,7 +72,7 @@ module Releases
end
def param_for_milestone_titles_provided?
- params.key?(:milestones)
+ !!params[:milestones]
end
def execute_hooks(release, action = 'create')
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index deefe559d5d..11fdbaf3169 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -10,7 +10,7 @@ module Releases
# should be found before the creation of new tag
# because tag creation can spawn new pipeline
# which won't have any data for evidence yet
- evidence_pipeline = find_evidence_pipeline
+ evidence_pipeline = Releases::EvidencePipelineFinder.new(project, params).execute
tag = ensure_tag
@@ -78,26 +78,10 @@ module Releases
)
end
- def find_evidence_pipeline
- # TODO: remove this with the release creation moved to it's own form https://gitlab.com/gitlab-org/gitlab/-/issues/214245
- return params[:evidence_pipeline] if params[:evidence_pipeline]
-
- sha = existing_tag&.dereferenced_target&.sha
- sha ||= repository.commit(ref)&.sha
-
- return unless sha
-
- project.ci_pipelines.for_sha(sha).last
- end
-
def create_evidence!(release, pipeline)
- return if release.historical_release?
+ return if release.historical_release? || release.upcoming_release?
- if release.upcoming_release?
- CreateEvidenceWorker.perform_at(release.released_at, release.id, pipeline&.id)
- else
- CreateEvidenceWorker.perform_async(release.id, pipeline&.id)
- end
+ ::Releases::CreateEvidenceWorker.perform_async(release.id, pipeline&.id)
end
end
end
diff --git a/app/services/users/approve_service.rb b/app/services/users/approve_service.rb
index 27668e9430e..debd1e8cd17 100644
--- a/app/services/users/approve_service.rb
+++ b/app/services/users/approve_service.rb
@@ -7,8 +7,9 @@ module Users
end
def execute(user)
- return error(_('You are not allowed to approve a user')) unless allowed?
- return error(_('The user you are trying to approve is not pending an approval')) unless approval_required?(user)
+ return error(_('You are not allowed to approve a user'), :forbidden) unless allowed?
+ return error(_('The user you are trying to approve is not pending an approval'), :conflict) if user.active?
+ return error(_('The user you are trying to approve is not pending an approval'), :conflict) unless approval_required?(user)
if user.activate
# Resends confirmation email if the user isn't confirmed yet.
@@ -18,9 +19,9 @@ module Users
DeviseMailer.user_admin_approval(user).deliver_later
after_approve_hook(user)
- success
+ success(message: 'Success', http_status: :created)
else
- error(user.errors.full_messages.uniq.join('. '))
+ error(user.errors.full_messages.uniq.join('. '), :unprocessable_entity)
end
end
diff --git a/app/uploaders/terraform/state_uploader.rb b/app/uploaders/terraform/state_uploader.rb
index 2306313fc82..d80725cb051 100644
--- a/app/uploaders/terraform/state_uploader.rb
+++ b/app/uploaders/terraform/state_uploader.rb
@@ -6,17 +6,33 @@ module Terraform
storage_options Gitlab.config.terraform_state
- delegate :project_id, to: :model
+ delegate :terraform_state, :project_id, to: :model
# Use Lockbox to encrypt/decrypt the stored file (registers CarrierWave callbacks)
encrypt(key: :key)
def filename
- "#{model.uuid}.tfstate"
+ # This check is required to maintain backwards compatibility with
+ # states that were created prior to versioning being supported.
+ # This can be removed in 14.0 when support for these states is dropped.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/258960
+ if terraform_state.versioning_enabled?
+ "#{model.version}.tfstate"
+ else
+ "#{model.uuid}.tfstate"
+ end
end
def store_dir
- project_id.to_s
+ # This check is required to maintain backwards compatibility with
+ # states that were created prior to versioning being supported.
+ # This can be removed in 14.0 when support for these states is dropped.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/258960
+ if terraform_state.versioning_enabled?
+ Gitlab::HashedPath.new(model.uuid, root_hash: project_id)
+ else
+ project_id.to_s
+ end
end
def key
diff --git a/app/uploaders/terraform/versioned_state_uploader.rb b/app/uploaders/terraform/versioned_state_uploader.rb
deleted file mode 100644
index e50ab6c7dc6..00000000000
--- a/app/uploaders/terraform/versioned_state_uploader.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module Terraform
- class VersionedStateUploader < StateUploader
- delegate :terraform_state, to: :model
-
- def filename
- if terraform_state.versioning_enabled?
- "#{model.version}.tfstate"
- else
- "#{model.uuid}.tfstate"
- end
- end
-
- def store_dir
- if terraform_state.versioning_enabled?
- Gitlab::HashedPath.new(model.uuid, root_hash: project_id)
- else
- project_id.to_s
- end
- end
- end
-end
diff --git a/app/validators/json_schemas/codeclimate.json b/app/validators/json_schemas/codeclimate.json
new file mode 100644
index 00000000000..56056c62c4e
--- /dev/null
+++ b/app/validators/json_schemas/codeclimate.json
@@ -0,0 +1,34 @@
+{
+ "description": "Codequality used by codeclimate parser",
+ "type": "object",
+ "required": ["description", "fingerprint", "severity", "location"],
+ "properties": {
+ "description": { "type": "string" },
+ "fingerprint": { "type": "string" },
+ "severity": { "type": "string" },
+ "location": {
+ "type": "object",
+ "properties": {
+ "path": { "type": "string" },
+ "lines": {
+ "type": "object",
+ "properties": {
+ "begin": { "type": "integer" }
+ }
+ },
+ "positions": {
+ "type": "object",
+ "properties": {
+ "begin": {
+ "type": "object",
+ "properties": {
+ "line": { "type": "integer" }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "additionalProperties": true
+}
diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml
index c1565cf42e1..b06070d15d4 100644
--- a/app/views/admin/application_settings/_ip_limits.html.haml
+++ b/app/views/admin/application_settings/_ip_limits.html.haml
@@ -2,44 +2,52 @@
= form_errors(@application_setting)
%fieldset
+ %h5
+ = _('Unauthenticated request rate limit')
.form-group
.form-check
= f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_unauthenticated_checkbox' }
- = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do
+ = f.label :throttle_unauthenticated_enabled, class: 'form-check-label label-bold' do
Enable unauthenticated request rate limit
%span.form-text.text-muted
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
- = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'label-bold'
+ = f.label :throttle_unauthenticated_requests_per_period, 'Max unauthenticated requests per period per IP', class: 'label-bold'
= f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
.form-group
- = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'label-bold'
+ = f.label :throttle_unauthenticated_period_in_seconds, 'Unauthenticated rate limit period in seconds', class: 'label-bold'
= f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
+ %hr
+ %h5
+ = _('Authenticated API request rate limit')
.form-group
.form-check
= f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_authenticated_api_checkbox' }
- = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do
+ = f.label :throttle_authenticated_api_enabled, class: 'form-check-label label-bold' do
Enable authenticated API request rate limit
%span.form-text.text-muted
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
- = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'label-bold'
+ = f.label :throttle_authenticated_api_requests_per_period, 'Max authenticated API requests per period per user', class: 'label-bold'
= f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
.form-group
- = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'label-bold'
+ = f.label :throttle_authenticated_api_period_in_seconds, 'Authenticated API rate limit period in seconds', class: 'label-bold'
= f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
+ %hr
+ %h5
+ = _('Authenticated web request rate limit')
.form-group
.form-check
= f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_authenticated_web_checkbox' }
- = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do
+ = f.label :throttle_authenticated_web_enabled, class: 'form-check-label label-bold' do
Enable authenticated web request rate limit
%span.form-text.text-muted
Helps reduce request volume (e.g. from crawlers or abusive bots)
.form-group
- = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'label-bold'
+ = f.label :throttle_authenticated_web_requests_per_period, 'Max authenticated web requests per period per user', class: 'label-bold'
= f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
.form-group
- = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'label-bold'
+ = f.label :throttle_authenticated_web_period_in_seconds, 'Authenticated web rate limit period in seconds', class: 'label-bold'
= f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
= f.submit 'Save changes', class: "gl-button btn btn-success", data: { qa_selector: 'save_changes_button' }
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 9f1b7195ab7..4959e596148 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -6,7 +6,7 @@
.settings-header
%h4
= _('Metrics - Prometheus')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Enable and configure Prometheus metrics.')
@@ -17,7 +17,7 @@
.settings-header
%h4
= _('Metrics - Grafana')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Enable and configure Grafana.')
@@ -28,7 +28,7 @@
.settings-header
%h4
= _('Profiling - Performance bar')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Enable access to the Performance Bar for a given group.')
@@ -42,7 +42,7 @@
.settings-header#usage-statistics
%h4
= _('Usage statistics')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Enable or disable version check and usage ping.')
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 220a211cca6..8cc04392752 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -9,7 +9,7 @@
dismissible: true.to_s } }
= notice[:message].html_safe
-- if @license.present? && show_license_breakdown?
+- if @license.present?
.license-panel.gl-mt-5
= render_if_exists 'admin/licenses/summary'
= render_if_exists 'admin/licenses/breakdown'
diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml
index 17bb054b869..5bc5404fada 100644
--- a/app/views/admin/hooks/_form.html.haml
+++ b/app/views/admin/hooks/_form.html.haml
@@ -18,28 +18,28 @@
.gl-mt-3
= form.check_box :repository_update_events, class: 'float-left'
- .prepend-left-20
+ .gl-ml-6
= form.label :repository_update_events, class: 'list-label' do
%strong Repository update events
%p.light
This URL will be triggered when repository is updated
%li
= form.check_box :push_events, class: 'float-left'
- .prepend-left-20
+ .gl-ml-6
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This URL will be triggered for each branch updated to the repository
%li
= form.check_box :tag_push_events, class: 'float-left'
- .prepend-left-20
+ .gl-ml-6
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This URL will be triggered when a new tag is pushed to the repository
%li
= form.check_box :merge_requests_events, class: 'float-left'
- .prepend-left-20
+ .gl-ml-6
= form.label :merge_requests_events, class: 'list-label' do
%strong Merge request events
%p.light
diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/admin/runners/_sort_dropdown.html.haml
index 3b3de042511..c6627ae0f27 100644
--- a/app/views/admin/runners/_sort_dropdown.html.haml
+++ b/app/views/admin/runners/_sort_dropdown.html.haml
@@ -3,7 +3,7 @@
.dropdown.inline.gl-ml-3
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
= sorted_by
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date), sorted_by)
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 2c4befb1be2..06925964dc5 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -9,7 +9,7 @@
%span.runner-state.runner-state-specific
Specific
-- page_title _("Runners")
+- page_title @runner.short_sha
- add_to_breadcrumbs _("Runners"), admin_runners_path
- breadcrumb_title "##{@runner.id}"
@@ -39,17 +39,18 @@
%thead
%tr
%th Assigned projects
- %th
- @runner.runner_projects.each do |runner_project|
- project = runner_project.project
- if project
- %tr.alert-info
+ %tr
%td
- %strong
- = project.full_name
- %td
- .float-right
- = link_to 'Disable', admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'gl-button btn btn-danger btn-sm'
+ .gl-alert.gl-alert-danger
+ = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ %strong
+ = project.full_name
+ .gl-alert-actions
+ = link_to s_('Disable'), admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'btn gl-alert-action btn-info btn-md gl-button'
%table.table.unassigned-projects
%thead
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 679c4805280..b9f5b92b957 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -40,7 +40,8 @@
%button.btn.btn-default-tertiary.js-confirm-modal-button{ data: user_block_data(user, user_block_effects) }
= s_('AdminUsers|Block')
- else
- = link_to _('Unblock'), unblock_admin_user_path(user), method: :put
+ %button.btn.btn-default-tertiary.js-confirm-modal-button{ data: unblock_user_modal_data(user) }
+ = s_('AdminUsers|Unblock')
- else
%button.btn.btn-default-tertiary.js-confirm-modal-button{ data: user_block_data(user, user_block_effects) }
= s_('AdminUsers|Block')
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 9c6f151a6b1..530c6878200 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -186,7 +186,8 @@
%li Log in
%li Access Git repositories
%br
- = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: s_('AdminUsers|Are you sure?') }
+ %button.btn.gl-button.btn-info.js-confirm-modal-button{ data: unblock_user_modal_data(@user) }
+ = s_('AdminUsers|Unblock user')
- elsif !@user.internal?
= render 'admin/users/block_user', user: @user
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 6ac852af2db..cb464eeafbb 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -27,6 +27,7 @@
provider_type: @cluster.provider_type,
pre_installed_knative: @cluster.knative_pre_installed? ? 'true': 'false',
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
+ helm_help_path: help_page_path('user/clusters/applications.md', anchor: 'helm'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-endpoint'),
ingress_dns_help_path: help_page_path('user/clusters/applications.md', anchor: 'pointing-your-dns-at-the-external-endpoint'),
ingress_mod_security_help_path: help_page_path('user/clusters/applications.md', anchor: 'web-application-firewall-modsecurity'),
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 9a9fbfc1ee8..221d79a30ce 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -71,7 +71,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%ul.dropdown-menu.dropdown-menu-sort.dropdown-menu-right
%li
= link_to todos_filter_path(sort: sort_value_label_priority) do
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 6fc156cf4ed..2ead8fc2cfd 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -10,7 +10,7 @@
= visibility_level_label(params[:visibility_level].to_i)
- else
= _('Any')
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%ul.dropdown-menu.dropdown-menu-right
%li
= link_to filter_projects_path(visibility_level: nil) do
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index ee08829d990..67f278a06f3 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -6,10 +6,10 @@
.row.mb-3
.home-panel-title-row.col-md-12.col-lg-6.d-flex
.avatar-container.rect-avatar.s64.home-panel-avatar.gl-mr-3.float-none
- = group_icon(@group, class: 'avatar avatar-tile s64', width: 64, height: 64)
+ = group_icon(@group, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'logo')
.d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline
- %h1.home-panel-title.gl-mt-3.gl-mb-2
+ %h1.home-panel-title.gl-mt-3.gl-mb-2{ itemprop: 'name' }
= @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, options: {class: 'icon'})
@@ -34,7 +34,7 @@
- if @group.description.present?
.group-home-desc.mt-1
.home-panel-description
- .home-panel-description-markdown.read-more-container
+ .home-panel-description-markdown.read-more-container{ itemprop: 'description' }
= markdown_field(@group, :description)
%button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
= _("Read more")
diff --git a/app/views/groups/_subgroups_and_projects.html.haml b/app/views/groups/_subgroups_and_projects.html.haml
index cb15fe339e1..d9ab828a83b 100644
--- a/app/views/groups/_subgroups_and_projects.html.haml
+++ b/app/views/groups/_subgroups_and_projects.html.haml
@@ -3,6 +3,6 @@
= render "shared/groups/empty_state"
%section{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
- .js-groups-list-holder
+ .js-groups-list-holder{ data: { show_schema_markup: 'true'} }
.loading-container.text-center.prepend-top-20
.spinner.spinner-md
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 9d5ec5008dc..109e7c3831e 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,5 +1,6 @@
-- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
+- page_itemtype 'https://schema.org/Organization'
+- @skip_current_level_breadcrumb = true
- if show_thanks_for_purchase_banner?
= render_if_exists 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:purchased_quantity].to_i
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index f6fc49393d8..c552454caa7 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -17,6 +17,7 @@
= render_account_recovery_regular_check
= render_if_exists "layouts/header/ee_subscribable_banner"
= render_if_exists "shared/namespace_storage_limit_alert"
+ = render_if_exists "shared/new_user_signups_cap_reached_alert"
= yield :customize_homepage_banner
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index f0cdb3d1a51..43f1011a85b 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -1,6 +1,7 @@
- container = @no_breadcrumb_container ? 'container-fluid' : container_class
- hide_top_links = @hide_top_links || false
-- push_to_schema_breadcrumb(@breadcrumb_title, breadcrumb_title_link)
+- unless @skip_current_level_breadcrumb
+ - push_to_schema_breadcrumb(@breadcrumb_title, breadcrumb_title_link)
%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] }
.breadcrumbs-container{ class: ("border-bottom-0" if @no_breadcrumb_border) }
@@ -16,8 +17,10 @@
- @breadcrumbs_extra_links.each do |extra|
= breadcrumb_list_item link_to(extra[:text], extra[:link])
= render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after
- %li
- %h2.breadcrumbs-sub-title= link_to @breadcrumb_title, breadcrumb_title_link
+ - unless @skip_current_level_breadcrumb
+ %li
+ %h2.breadcrumbs-sub-title
+ = link_to @breadcrumb_title, breadcrumb_title_link
%script{ type:'application/ld+json' }
:plain
#{schema_breadcrumb_json}
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 9c5cfe35cda..e1345a94fb1 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -3,10 +3,14 @@
%div
- if @user.errors.any?
- .gl-alert.gl-alert-danger
- %ul
- - @user.errors.full_messages.each do |msg|
- %li= msg
+ .gl-alert.gl-alert-danger.gl-my-5
+ %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
+ = sprite_icon('close', css_class: 'gl-icon')
+ = sprite_icon('error', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-body
+ %ul
+ - @user.errors.full_messages.each do |msg|
+ %li= msg
= hidden_field_tag :notification_type, 'global'
.row.gl-mt-3
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 569255ec2e5..ebb0dd8b39f 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -19,7 +19,7 @@
= render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project
.home-panel-metadata.d-flex.flex-wrap.text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal
- if can?(current_user, :read_project, @project)
- %span.text-secondary{ itemprop: 'identifier' }
+ %span.text-secondary{ itemprop: 'identifier', data: { qa_selector: 'project_id_content' } }
= s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id }
- if current_user
%span.access-request-links.gl-ml-3
@@ -63,7 +63,7 @@
.home-panel-home-desc.mt-1
- if @project.description.present?
.home-panel-description.text-break
- .home-panel-description-markdown.read-more-container{ itemprop: 'abstract' }
+ .home-panel-description-markdown.read-more-container{ itemprop: 'description' }
= markdown_field(@project, :description)
%button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
= _("Read more")
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 aedfb64d3e4..db4b04eaeb8 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')
+= loading_icon(css_class: "gl-vertical-align-text-bottom mr-1")
= _('Metrics Dashboard YAML definition') + '…'
= link_to _('Learn more'), help_page_path('operations/metrics/dashboards/yaml.md')
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index cf58cff7445..938dfc69500 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -2,7 +2,7 @@
- dropdown_class = local_assigns.fetch(:dropdown_class, '')
.git-clone-holder.js-git-clone-holder
- %a#clone-dropdown.gl-button.btn.btn-primary.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
+ %a#clone-dropdown.gl-button.btn.btn-info.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
%span.gl-mr-2.js-clone-dropdown-label
= _('Clone')
= sprite_icon("chevron-down", css_class: "icon")
@@ -12,7 +12,7 @@
%label.label-bold
= _('Clone with SSH')
.input-group
- = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: 'Project clone URL' }
+ = text_field_tag :ssh_project_clone, project.ssh_url_to_repo, class: "js-select-on-focus form-control qa-ssh-clone-url", readonly: true, aria: { label: _('Repository clone URL') }
.input-group-append
= clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo'
@@ -21,7 +21,7 @@
%label.label-bold
= _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
.input-group
- = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control qa-http-clone-url", readonly: true, aria: { label: 'Project clone URL' }
+ = text_field_tag :http_project_clone, project.http_url_to_repo, class: "js-select-on-focus form-control qa-http-clone-url", readonly: true, aria: { label: _('Repository clone URL') }
.input-group-append
= clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo'
diff --git a/app/views/projects/ci/pipeline_editor/show.html.haml b/app/views/projects/ci/pipeline_editor/show.html.haml
index 0e032f2575e..f1f8658fa3b 100644
--- a/app/views/projects/ci/pipeline_editor/show.html.haml
+++ b/app/views/projects/ci/pipeline_editor/show.html.haml
@@ -3,4 +3,6 @@
#js-pipeline-editor{ data: { "ci-config-path": @project.ci_config_path_or_default,
"project-path" => @project.full_path,
"default-branch" => @project.default_branch,
+ "commit-id" => @project.commit ? @project.commit.id : '',
+ "new-merge-request-path" => namespace_project_new_merge_request_path,
} }
diff --git a/app/views/projects/commit/_verified_signature_badge.html.haml b/app/views/projects/commit/_verified_signature_badge.html.haml
index 4964b1b8ee7..357ad467539 100644
--- a/app/views/projects/commit/_verified_signature_badge.html.haml
+++ b/app/views/projects/commit/_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/commit/x509/_unverified_signature_badge.html.haml b/app/views/projects/commit/x509/_unverified_signature_badge.html.haml
index 680cc32c7e6..6204a6977c0 100644
--- a/app/views/projects/commit/x509/_unverified_signature_badge.html.haml
+++ b/app/views/projects/commit/x509/_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', show_user: true }
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index c6d39f5bba0..2e0bb3b8529 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,7 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
- default_branch_name = @project.default_branch || "master"
-- breadcrumb_title _("Details")
-- page_title _("Details")
+- @skip_current_level_breadcrumb = true
= render partial: 'flash_messages', locals: { project: @project }
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 67dc07fb785..89c2c826067 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -15,7 +15,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%ul.dropdown-menu.dropdown-menu-right
%li
- excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index d9ad171a6cc..3ce85fb46d5 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,67 +1,66 @@
-# 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_container', qa_issue_title: issue.title } }
- .issue-box
+ .issuable-info-container
- 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
- %span.issue-title-text.js-onboarding-issue-item{ dir: "auto" }
- - if issue.confidential?
- %span.has-tooltip{ title: _('Confidential') }
- = confidential_icon(issue)
- = link_to issue.title, issue_path(issue)
- = render_if_exists 'projects/issues/subepic_flag', issue: issue
- - if issue.tasks?
- %span.task-status.d-none.d-sm-inline-block
- &nbsp;
- = issue.task_status
+ .issuable-main-info
+ .issue-title.title
+ %span.issue-title-text.js-onboarding-issue-item{ dir: "auto" }
+ - if issue.confidential?
+ %span.has-tooltip{ title: _('Confidential') }
+ = confidential_icon(issue)
+ = link_to issue.title, issue_path(issue)
+ = render_if_exists 'projects/issues/subepic_flag', issue: issue
+ - if issue.tasks?
+ %span.task-status.d-none.d-sm-inline-block
+ &nbsp;
+ = issue.task_status
- .issuable-info
- %span.issuable-reference
- #{issuable_reference(issue)}
- %span.issuable-authored.d-none.d-sm-inline-block
- &middot;
- opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
- by #{link_to_member(@project, issue.author, avatar: false)}
- = render_if_exists 'shared/issuable/gitlab_team_member_badge', {author: issue.author}
- - if issue.milestone
- %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
- = 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') }
- &nbsp;
- = sprite_icon('calendar')
- = issue.due_date.to_s(:medium)
+ .issuable-info
+ %span.issuable-reference
+ #{issuable_reference(issue)}
+ %span.issuable-authored.d-none.d-sm-inline-block
+ &middot;
+ opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
+ by #{link_to_member(@project, issue.author, avatar: false)}
+ = render_if_exists 'shared/issuable/gitlab_team_member_badge', {author: issue.author}
+ - if issue.milestone
+ %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
+ = 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') }
+ &nbsp;
+ = sprite_icon('calendar')
+ = issue.due_date.to_s(:medium)
- = render_if_exists "projects/issues/issue_weight", issue: issue
- = render_if_exists "projects/issues/health_status", issue: issue
+ = render_if_exists "projects/issues/issue_weight", issue: issue
+ = render_if_exists "projects/issues/health_status", issue: issue
- - if issue.labels.any?
- &nbsp;
- - presented_labels_sorted_by_title(issue.labels, issue.project).each do |label|
- = link_to_label(label, small: true)
+ - if issue.labels.any?
+ &nbsp;
+ - presented_labels_sorted_by_title(issue.labels, issue.project).each do |label|
+ = link_to_label(label, small: true)
- = render "projects/issues/issue_estimate", issue: issue
+ = render "projects/issues/issue_estimate", issue: issue
- .issuable-meta
- %ul.controls
- - if issue.closed? && issue.moved?
- %li.issuable-status
- = _('CLOSED (MOVED)')
- - elsif issue.closed?
- %li.issuable-status
- = _('CLOSED')
- - if issue.assignees.any?
- %li.gl-display-flex
- = render 'shared/issuable/assignees', project: @project, issuable: issue
+ .issuable-meta
+ %ul.controls
+ - if issue.closed? && issue.moved?
+ %li.issuable-status
+ = _('CLOSED (MOVED)')
+ - elsif issue.closed?
+ %li.issuable-status
+ = _('CLOSED')
+ - if issue.assignees.any?
+ %li.gl-display-flex
+ = render 'shared/issuable/assignees', project: @project, issuable: issue
- = render 'shared/issuable_meta_data', issuable: issue
+ = render 'shared/issuable_meta_data', issuable: issue
- .float-right.issuable-updated-at.d-none.d-sm-inline-block
- %span
- = _('updated %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago') }
+ .float-right.issuable-updated-at.d-none.d-sm-inline-block
+ %span
+ = _('updated %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago') }
diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
index e6205f24ae6..cb1cb41eb71 100644
--- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -1,16 +1,11 @@
.content-block.oneline-block.files-changed{ "v-if" => "!isLoading && !hasError" }
.inline-parallel-buttons{ "v-if" => "showDiffViewTypeSwitcher" }
.btn-group
- %button.btn{ ":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')" }
- Inline
- %button.btn{ ":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')" }
- Side-by-side
+ %button.btn.gl-button{ ":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')" }
+ = _('Inline')
+ %button.btn.gl-button{ ":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')" }
+ = _('Side-by-side')
.js-toggle-container
.commit-stat-summary
- Showing
- %strong.cred {{conflictsCountText}}
- between
- %strong.ref-name {{conflictsData.sourceBranch}}
- and
- %strong.ref-name {{conflictsData.targetBranch}}
+ = _('Showing %{conflict_start}%{conflicts_text}%{strong_end} between %{ref_start}%{source_branch}%{strong_end} and %{ref_start}%{target_branch}%{strong_end}').html_safe % { conflict_start: '<strong class="cred">'.html_safe, ref_start: '<strong class="ref-name">'.html_safe, strong_end: '</strong>'.html_safe, conflicts_text: '{{conflictsCountText}}', source_branch: '{{conflictsData.sourceBranch}}', target_branch: '{{conflictsData.targetBranch}}' }
diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
index 0839880713f..220ddf1bad3 100644
--- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
@@ -1,12 +1,12 @@
-.file-actions
- .btn-group{ "v-if" => "file.type === 'text'" }
- %button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
+.file-actions.d-flex.align-items-center.gl-ml-auto.gl-align-self-start
+ .btn-group.gl-mr-3{ "v-if" => "file.type === 'text'" }
+ %button.btn.gl-button{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
'@click' => "onClickResolveModeButton(file, 'interactive')",
type: 'button' }
- Interactive mode
- %button.btn{ ':class' => "{ 'active': file.resolveMode == 'edit' }",
+ = _('Interactive mode')
+ %button.btn.gl-button{ ':class' => "{ 'active': file.resolveMode == 'edit' }",
'@click' => "onClickResolveModeButton(file, 'edit')",
type: 'button' }
- Edit inline
- %a.btn.view-file{ ":href" => "file.blobPath" }
- View file @{{conflictsData.shortCommitSha}}
+ = _('Edit inline')
+ %a.btn.gl-button.view-file{ ":href" => "file.blobPath" }
+ = _('View file @%{commit_sha}') % { commit_sha: '{{conflictsData.shortCommitSha}}' }
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
index 94c262d300e..15655e2b162 100644
--- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -18,7 +18,7 @@
.offset-md-4.col-md-8
.row
.col-6
- %button.btn.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
+ %button.btn.gl-button.btn-success.js-submit-button{ type: "button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
%span {{commitButtonText}}
.col-6.text-right
= link_to "Cancel", project_merge_request_path(@merge_request.project, @merge_request), class: "gl-button btn btn-cancel"
diff --git a/app/views/projects/merge_requests/conflicts/show.html.haml b/app/views/projects/merge_requests/conflicts/show.html.haml
index decdbce3fa7..827df540629 100644
--- a/app/views/projects/merge_requests/conflicts/show.html.haml
+++ b/app/views/projects/merge_requests/conflicts/show.html.haml
@@ -20,9 +20,10 @@
.files-wrapper{ "v-if" => "!isLoading && !hasError" }
.files
.diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" }
- .js-file-title.file-title
- %i.fa.fa-fw{ ":class" => "file.iconClass" }
- %strong {{file.filePath}}
+ .js-file-title.file-title.file-title-flex-parent.cursor-default
+ .file-header-content
+ %file-icon{ ':file-name': 'file.filePath', ':size': '18', 'css-classes': 'gl-mr-2' }
+ %strong.file-title-name {{file.filePath}}
= render partial: 'projects/merge_requests/conflicts/file_actions'
.diff-content.diff-wrap-lines
.file-content{ "v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 6b506c38795..53ea9678c07 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -58,6 +58,8 @@
= render "projects/merge_requests/description"
= render "projects/merge_requests/widget"
= render "projects/merge_requests/awards_block"
+ - if mr_action === "show"
+ - add_page_startup_api_call discussions_path(@merge_request)
#js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json,
noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'),
noteable_type: 'MergeRequest',
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 4366676bd45..30ba22ba53c 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -8,7 +8,7 @@
= text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: _("Git revision"), class: 'search-input form-control input-mx-250 search-sha'
= button_tag class: 'btn btn-success' do
= sprite_icon('search')
- .inline.prepend-left-20
+ .inline.gl-ml-5
.form-check.light
= check_box_tag :filter_ref, 1, @options[:filter_ref], class: 'form-check-input'
= label_tag :filter_ref, class: 'form-check-label' do
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index f2972a9617b..a407aa9ac13 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -8,10 +8,9 @@
.project-edit-errors
= render 'projects/errors'
- - if experiment_enabled?(:new_create_project_ui)
- .js-experiment-new-project-creation{ data: { is_ci_cd_available: ci_cd_projects_available?, has_errors: @project.errors.any? } }
+ .js-experiment-new-project-creation{ data: { is_ci_cd_available: (ci_cd_projects_available? if Gitlab.ee?), has_errors: @project.errors.any? } }
- .row{ 'v-cloak': experiment_enabled?(:new_create_project_ui) }
+ .row{ 'v-cloak': true }
.col-lg-3.profile-settings-sidebar
%h4.gl-mt-0
= _('New project')
diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml
index 65c4232b240..d7853c1b466 100644
--- a/app/views/projects/no_repo.html.haml
+++ b/app/views/projects/no_repo.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title _("Details")
-- page_title _("Details")
+- page_title _('No repository')
+- @skip_current_level_breadcrumb = true
%h2.gl-display-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
diff --git a/app/views/projects/registry/settings/_index.haml b/app/views/projects/registry/settings/_index.haml
index c6fae2cc7a1..a4d4a1bb2dd 100644
--- a/app/views/projects/registry/settings/_index.haml
+++ b/app/views/projects/registry/settings/_index.haml
@@ -5,4 +5,5 @@
older_than_options: older_than_options.to_json,
is_admin: current_user&.admin.to_s,
admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'),
- enable_historic_entries: container_expiration_policies_historic_entry_enabled?(@project).to_s} }
+ enable_historic_entries: container_expiration_policies_historic_entry_enabled?(@project).to_s,
+ tags_regex_help_page_path: help_page_path('user/packages/container_registry/index', anchor: 'regex-pattern-examples') } }
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 9d81fda68cb..549ca36cb6a 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,4 +1,4 @@
-- pretty_name = html_escape(@project&.full_name) || html_escape_once(_('&lt;project name&gt;')).html_safe
+- pretty_name = @project&.full_name ? html_escape(@project&.full_name) : '<' + _('project name') + '>'
- 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:")
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 86486d95eb7..67c43bd2f33 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,4 +1,4 @@
-- pretty_name = @project&.full_name || _('&lt;project name&gt;')
+- pretty_name = @project&.full_name ? html_escape(@project&.full_name) : '<' + _('project name') + '>'
- run_actions_text = html_escape_once(s_("ProjectService|Perform common operations on GitLab project: %{project_name}") % { project_name: pretty_name })
.info-well
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index f7c51e9ada9..5b9f868a71a 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
+- @skip_current_level_breadcrumb = true
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index e0def8cf155..2fe5c5888f5 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -16,7 +16,7 @@
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown'} }
%span.light
= tags_sort_options_hash[@sort]
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= s_('TagsPage|Sort by')
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index fe42394d919..73b2a92dcc0 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -24,7 +24,7 @@
= hidden_field_tag :ref, default_ref
= button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select monospace', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
.text-left.dropdown-toggle-text= default_ref
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
= render 'shared/ref_dropdown', dropdown_class: 'wide'
.form-text.text-muted
= s_('TagsPage|Existing branch name, tag, or commit SHA')
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index c166642bae2..2542860c742 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -11,7 +11,7 @@
%strong= @wiki.full_path
.pt-3.pt-lg-0.w-100
- = render "shared/clone_panel", project: @wiki
+ = render "shared/clone_panel", container: @wiki
.wiki-git-access
%h3= s_("WikiClone|Install Gollum")
diff --git a/app/views/registrations/experience_levels/show.html.haml b/app/views/registrations/experience_levels/show.html.haml
index 24b87790e18..f878245a48c 100644
--- a/app/views/registrations/experience_levels/show.html.haml
+++ b/app/views/registrations/experience_levels/show.html.haml
@@ -15,8 +15,8 @@
= image_tag 'novice.svg', width: '78', height: '78', alt: ''
%div
%p.gl-font-lg.gl-font-weight-bold.gl-mb-2= _('Novice')
- %p= _('I’m not very familiar with the basics of project management and DevOps.')
- = link_to _('Show me everything'), users_sign_up_experience_level_path(experience_level: :novice, namespace_path: params[:namespace_path]), method: :patch, class: 'stretched-link'
+ %p= _('I’m not familiar with the basics of DevOps.')
+ = link_to _('Show me the basics'), users_sign_up_experience_level_path(experience_level: :novice, namespace_path: params[:namespace_path]), method: :patch, class: 'stretched-link'
.card
.card-body.gl-display-flex.gl-py-8.gl-pr-5.gl-pl-7
@@ -24,5 +24,5 @@
= image_tag 'experienced.svg', width: '78', height: '78', alt: ''
%div
%p.gl-font-lg.gl-font-weight-bold.gl-mb-2= _('Experienced')
- %p= _('I’m familiar with the basics of project management and DevOps.')
- = link_to _('Show me more advanced stuff'), users_sign_up_experience_level_path(experience_level: :experienced, namespace_path: params[:namespace_path]), method: :patch, class: 'stretched-link'
+ %p= _('I’m familiar with the basics of DevOps.')
+ = link_to _('Show me advanced features'), users_sign_up_experience_level_path(experience_level: :experienced, namespace_path: params[:namespace_path]), method: :patch, class: 'stretched-link'
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index 964a2a2772a..05895d83c2b 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -14,7 +14,7 @@
= @project&.full_name || _("Any")
- if @project.present?
= link_to sprite_icon("clear"), url_for(safe_params.except(:project_id)), class: 'search-clear js-search-clear has-tooltip', title: _('Clear')
- = icon("chevron-down")
+ = sprite_icon("chevron-down", css_class: 'dropdown-menu-toggle-icon gl-top-3')
.dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right
= dropdown_title(_("Filter results by project"))
= dropdown_filter(_("Search projects"))
diff --git a/app/views/search/_sort_dropdown.html.haml b/app/views/search/_sort_dropdown.html.haml
index 085e2f348f7..c4c5a490ff5 100644
--- a/app/views/search/_sort_dropdown.html.haml
+++ b/app/views/search/_sort_dropdown.html.haml
@@ -8,7 +8,7 @@
.btn-group{ role: 'group' }
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
= sort_title
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= render_if_exists('search/sort_by_relevancy', sort_title: sort_title)
diff --git a/app/views/shared/_alert_info.html.haml b/app/views/shared/_alert_info.html.haml
new file mode 100644
index 00000000000..e47c100909a
--- /dev/null
+++ b/app/views/shared/_alert_info.html.haml
@@ -0,0 +1,6 @@
+.gl-alert.gl-alert-info
+ = 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
+ = body
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 9ec8d3c18cd..1ed37c7a5c4 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,11 +1,9 @@
-- project = project || @project
-
.git-clone-holder.js-git-clone-holder.input-group
.input-group-prepend
- if allowed_protocols_present?
.input-group-text.clone-dropdown-btn.btn
%span.js-clone-dropdown-label
- = enabled_project_button(project, enabled_protocol)
+ = enabled_protocol_button(container, enabled_protocol)
- else
%a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
%span.js-clone-dropdown-label
@@ -13,12 +11,12 @@
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown
%li
- = ssh_clone_button(project)
+ = ssh_clone_button(container)
%li
- = http_clone_button(project)
- = render_if_exists 'shared/kerberos_clone_button', project: project
+ = http_clone_button(container)
+ = render_if_exists 'shared/kerberos_clone_button', container: container
- = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Project clone URL') }
+ = text_field_tag :clone_url, default_url_to_repo(container), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }
.input-group-append
- = clipboard_button(target: '#project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
+ = clipboard_button(target: '#clone_url', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml
index 06da990e071..29c01343358 100644
--- a/app/views/shared/_milestones_sort_dropdown.html.haml
+++ b/app/views/shared/_milestones_sort_dropdown.html.haml
@@ -5,7 +5,7 @@
= milestone_sort_options_hash[@sort]
- else
= sort_title_due_date_soon
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
%li
= link_to page_filter_path(sort: sort_value_due_date_soon) do
diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml
index 76ae63ca5e8..9c1e5a49b44 100644
--- a/app/views/shared/_no_password.html.haml
+++ b/app/views/shared/_no_password.html.haml
@@ -5,7 +5,7 @@
= sprite_icon('close', size: 16, css_class: 'gl-icon')
.gl-alert-body
- translation_params = { protocol: gitlab_config.protocol.upcase, set_password_link: link_to_set_password }
- - set_password_message = _("You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account") % translation_params
+ - set_password_message = _("You won't be able to pull or push repositories via %{protocol} until you %{set_password_link} on your account") % translation_params
= set_password_message.html_safe
.gl-alert-actions
= link_to _('Remind later'), '#', class: 'hide-no-password-message btn gl-alert-action btn-info btn-md gl-button'
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index a083a772233..0a7fa2a3c1e 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -4,7 +4,7 @@
%button{ class: 'gl-alert-dismiss hide-no-ssh-message', type: 'button', 'aria-label': _('Dismiss') }
= 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
+ = s_("MissingSSHKeyWarningLink|You won't be able to pull or push repositories via SSH until you add an SSH key to your profile")
.gl-alert-actions
= link_to s_('MissingSSHKeyWarningLink|Add SSH key'), profile_keys_path, class: "btn gl-alert-action btn-warning btn-md new-gl-button"
= link_to s_("MissingSSHKeyWarningLink|Don't show again"), profile_path(user: {hide_no_ssh_key: true}), method: :put, role: 'button', class: 'btn gl-alert-action btn-md btn-warning gl-button btn-warning-secondary'
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 9d2d3ce20c7..75c34102935 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -1,24 +1,17 @@
- options_hash = local_assigns.fetch(:options_hash, groups_sort_options_hash)
- show_archive_options = local_assigns.fetch(:show_archive_options, false)
-- if @sort.present?
- - default_sort_by = @sort
-- else
- - if params[:sort]
- - default_sort_by = params[:sort]
- - else
- - default_sort_by = sort_value_recently_created
.dropdown.inline.js-group-filter-dropdown-wrap.gl-mr-3
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.dropdown-label
- = options_hash[default_sort_by]
- = icon('chevron-down')
+ = options_hash[project_list_sort_by]
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= _("Sort by")
- options_hash.each do |value, title|
%li.js-filter-sort-order
- = link_to filter_groups_path(sort: value), class: ("is-active" if default_sort_by == value) do
+ = link_to filter_groups_path(sort: value), class: ("is-active" if project_list_sort_by == value) do
= title
- if show_archive_options
%li.divider
diff --git a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
index 60dc893d9f9..b437ee1ec5f 100644
--- a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
@@ -1,4 +1,4 @@
-= form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
+= form.label :assignee_id, issuable.allows_multiple_assignees? ? _('Assignees') : _('Assignee'), class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder.selectbox
- issuable.assignees.each do |assignee|
diff --git a/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml b/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml
index a8b033bba36..5ff3a6781ad 100644
--- a/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issuable_reviewer.html.haml
@@ -1,4 +1,4 @@
-= form.label :reviewer_id, "Reviewer", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
+= form.label :reviewer_id, issuable.allows_multiple_reviewers? ? _('Reviewers') : _('Reviewer'), class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder.selectbox
- issuable.reviewers.each do |reviewer|
diff --git a/app/views/shared/labels/_sort_dropdown.html.haml b/app/views/shared/labels/_sort_dropdown.html.haml
index 07e96eea062..cfc00bd41ca 100644
--- a/app/views/shared/labels/_sort_dropdown.html.haml
+++ b/app/views/shared/labels/_sort_dropdown.html.haml
@@ -2,7 +2,7 @@
.dropdown.inline
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
= sort_title
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
%li
- label_sort_options_hash.each do |value, title|
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 42e12d92a7d..d98ba074687 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -27,7 +27,7 @@
data: { toggle: "dropdown", field_name: "group_link[group_access]" } }
%span.dropdown-toggle-text
= group_link.human_access
- = icon("chevron-down")
+ = sprite_icon("chevron-down", css_class: "dropdown-menu-toggle-icon gl-top-3")
.dropdown-menu.dropdown-select.dropdown-menu-right.dropdown-menu-selectable
= dropdown_title(_("Change permissions"))
.dropdown-content
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index e294936f82c..79bbb74d601 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -79,7 +79,7 @@
data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]", qa_selector: "access_level_dropdown" } }
%span.dropdown-toggle-text
= member.human_access
- = icon("chevron-down")
+ = sprite_icon("chevron-down", css_class: "dropdown-menu-toggle-icon gl-top-3")
.dropdown-menu.dropdown-select.dropdown-menu-right.dropdown-menu-selectable
= dropdown_title(_("Change permissions"))
.dropdown-content
diff --git a/app/views/shared/projects/_sort_dropdown.html.haml b/app/views/shared/projects/_sort_dropdown.html.haml
index f5f940db189..3e810dc6f08 100644
--- a/app/views/shared/projects/_sort_dropdown.html.haml
+++ b/app/views/shared/projects/_sort_dropdown.html.haml
@@ -5,7 +5,7 @@
.btn-group.w-100.dropdown.js-project-filter-dropdown-wrap{ role: "group" }
%button#sort-projects-dropdown.btn.btn-default.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
= toggle_text
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
= _("Sort by")
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index c5234f14090..4dd434ad1db 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -10,7 +10,7 @@
= s_('Webhooks|Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.')
.form-group
= form.label :url, s_('Webhooks|Trigger'), class: 'label-bold'
- %ul.list-unstyled.prepend-left-20
+ %ul.list-unstyled.gl-ml-6
%li
= form.check_box :push_events, class: 'form-check-input'
= form.label :push_events, class: 'list-label form-check-label ml-1' do
diff --git a/app/views/shared/wikis/_form.html.haml b/app/views/shared/wikis/_form.html.haml
index dde1b3afa2d..b6504c7a17e 100644
--- a/app/views/shared/wikis/_form.html.haml
+++ b/app/views/shared/wikis/_form.html.haml
@@ -36,7 +36,7 @@
.col-sm-10
.select-wrapper
= f.select :format, options_for_select(Wiki::MARKUPS, {selected: @page.format}), {}, class: 'form-control select-control'
- = icon('chevron-down')
+ = sprite_icon('chevron-down', css_class: 'gl-absolute gl-top-3 gl-right-3 gl-text-gray-200')
.form-group.row
.col-sm-2.col-form-label= f.label :content, class: 'control-label-full-width'
diff --git a/app/views/shared/wikis/_sidebar.html.haml b/app/views/shared/wikis/_sidebar.html.haml
index c0ed7b4c6f2..7022762840e 100644
--- a/app/views/shared/wikis/_sidebar.html.haml
+++ b/app/views/shared/wikis/_sidebar.html.haml
@@ -10,11 +10,14 @@
= sprite_icon('download', css_class: 'gl-mr-2')
%span= _("Clone repository")
+ - if @sidebar_error.present?
+ = render 'shared/alert_info', body: s_('Wiki|The sidebar failed to load. You can reload the page to try again.')
+
.blocks-container
.block.block-first.w-100
- if @sidebar_page
= render_wiki_content(@sidebar_page)
- - else
+ - elsif @sidebar_wiki_entries
%ul.wiki-pages
= render @sidebar_wiki_entries, context: 'sidebar'
.block.w-100
diff --git a/app/views/shared/wikis/git_error.html.haml b/app/views/shared/wikis/git_error.html.haml
new file mode 100644
index 00000000000..dab3b940b9a
--- /dev/null
+++ b/app/views/shared/wikis/git_error.html.haml
@@ -0,0 +1,14 @@
+- if @page
+ - wiki_page_title @page
+
+- add_page_specific_style 'page_bundles/wiki'
+
+- git_access_url = wiki_path(@wiki, action: :git_access)
+
+.wiki-page-header.top-area.gl-flex-direction-column.gl-lg-flex-direction-row
+ .gl-mt-5.gl-mb-3
+ .gl-display-flex.gl-justify-content-space-between
+ %h2.gl-mt-0.gl-mb-5{ data: { qa_selector: 'wiki_page_title', testid: 'wiki_page_title' } }= @page ? @page.human_title : _('Failed to retrieve page')
+ .js-wiki-page-content.md.gl-pt-2{ data: { qa_selector: 'wiki_page_content', testid: 'wiki_page_content' } }
+ = _('The page could not be displayed because it timed out.')
+ = html_escape(_('You can view the source or %{linkStart}%{cloneIcon} clone the repository%{linkEnd}')) % { linkStart: "<a href=\"#{git_access_url}\">".html_safe, linkEnd: '</a>'.html_safe, cloneIcon: sprite_icon('download', css_class: 'gl-mr-2').html_safe }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 6f080a97f7a..80204fd31b9 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -124,7 +124,7 @@
:idempotent:
:tags: []
- :name: cronjob:analytics_instance_statistics_count_job_trigger
- :feature_category: :instance_statistics
+ :feature_category: :devops_reports
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -323,6 +323,22 @@
:weight: 1
:idempotent:
:tags: []
+- :name: cronjob:releases_create_evidence
+ :feature_category: :release_evidence
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
+- :name: cronjob:releases_manage_evidence
+ :feature_category: :release_evidence
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: cronjob:remove_expired_group_links
:feature_category: :authentication_and_authorization
:has_external_dependencies:
@@ -1313,7 +1329,7 @@
:idempotent: true
:tags: []
- :name: analytics_instance_statistics_counter_job
- :feature_category: :instance_statistics
+ :feature_category: :devops_reports
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -1377,14 +1393,6 @@
:weight: 2
:idempotent: true
:tags: []
-- :name: create_evidence
- :feature_category: :release_evidence
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 2
- :idempotent:
- :tags: []
- :name: create_note_diff_file
:feature_category: :source_code_management
:has_external_dependencies:
diff --git a/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb b/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb
index bf57619fc6e..81a765d5d08 100644
--- a/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb
+++ b/app/workers/analytics/instance_statistics/count_job_trigger_worker.rb
@@ -8,7 +8,7 @@ module Analytics
DEFAULT_DELAY = 3.minutes.freeze
- feature_category :instance_statistics
+ feature_category :devops_reports
urgency :low
idempotent!
diff --git a/app/workers/analytics/instance_statistics/counter_job_worker.rb b/app/workers/analytics/instance_statistics/counter_job_worker.rb
index 7fc715419b8..c07b2569453 100644
--- a/app/workers/analytics/instance_statistics/counter_job_worker.rb
+++ b/app/workers/analytics/instance_statistics/counter_job_worker.rb
@@ -5,7 +5,7 @@ module Analytics
class CounterJobWorker
include ApplicationWorker
- feature_category :instance_statistics
+ feature_category :devops_reports
urgency :low
idempotent!
diff --git a/app/workers/create_evidence_worker.rb b/app/workers/create_evidence_worker.rb
deleted file mode 100644
index b18028e4114..00000000000
--- a/app/workers/create_evidence_worker.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- feature_category :release_evidence
- weight 2
-
- # pipeline_id is optional for backward compatibility with existing jobs
- # caller should always try to provide the pipeline and pass nil only
- # if pipeline is absent
- def perform(release_id, pipeline_id = nil)
- release = Release.find_by_id(release_id)
- return unless release
-
- pipeline = Ci::Pipeline.find_by_id(pipeline_id)
-
- ::Releases::CreateEvidenceService.new(release, pipeline: pipeline).execute
- end
-end
diff --git a/app/workers/releases/create_evidence_worker.rb b/app/workers/releases/create_evidence_worker.rb
new file mode 100644
index 00000000000..db75fae1639
--- /dev/null
+++ b/app/workers/releases/create_evidence_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Releases
+ class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+ include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+
+ feature_category :release_evidence
+
+ # pipeline_id is optional for backward compatibility with existing jobs
+ # caller should always try to provide the pipeline and pass nil only
+ # if pipeline is absent
+ def perform(release_id, pipeline_id = nil)
+ release = Release.find_by_id(release_id)
+
+ return unless release
+
+ pipeline = Ci::Pipeline.find_by_id(pipeline_id)
+
+ ::Releases::CreateEvidenceService.new(release, pipeline: pipeline).execute
+ end
+ end
+end
diff --git a/app/workers/releases/manage_evidence_worker.rb b/app/workers/releases/manage_evidence_worker.rb
new file mode 100644
index 00000000000..8a925d22cea
--- /dev/null
+++ b/app/workers/releases/manage_evidence_worker.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Releases
+ class ManageEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+ include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+
+ feature_category :release_evidence
+
+ def perform
+ releases = Release.without_evidence.released_within_2hrs
+
+ releases.each do |release|
+ project = release.project
+ params = { tag: release.tag }
+
+ evidence_pipeline = Releases::EvidencePipelineFinder.new(project, params).execute
+
+ # perform_at released_at
+ ::Releases::CreateEvidenceWorker.perform_async(release.id, evidence_pipeline&.id)
+ end
+ end
+ end
+end
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
index eb1a7f4fef9..5876cfb1fe7 100644
--- a/app/workers/trending_projects_worker.rb
+++ b/app/workers/trending_projects_worker.rb
@@ -2,10 +2,6 @@
class TrendingProjectsWorker # rubocop:disable Scalability/IdempotentWorker
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
feature_category :source_code_management